mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-07-13 08:03:43 +01:00
Refactors slaughter, shadow, and pulse demons to basic mobs (#30660)
* Refactors slaughter, shadow, and pulse demons to basic mobs * Linters * Fix * Projectile changes * Ensures demons speak common * Update code/modules/mob/living/basic/hostile/demons/demon_powers.dm Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> --------- Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/mob/living/basic/demon
|
||||
name = "a generic demon"
|
||||
desc = "you shouldnt be reading this, file a github report."
|
||||
speak_emote = list("gurgles", "wails", "screeches")
|
||||
response_help_continuous = "thinks better of touching"
|
||||
response_help_simple = "thinks better of touching"
|
||||
response_disarm_continuous = "flails at"
|
||||
response_disarm_simple = "flail at"
|
||||
response_harm_continuous = "punches"
|
||||
response_harm_simple = "punches"
|
||||
a_intent = INTENT_HARM
|
||||
mob_biotypes = MOB_ORGANIC | MOB_HUMANOID
|
||||
attack_sound = 'sound/misc/demon_attack1.ogg'
|
||||
death_sound = 'sound/misc/demon_dies.ogg'
|
||||
atmos_requirements = list("min_oxy" = 16, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
|
||||
unsuitable_atmos_damage = 10
|
||||
minimum_survivable_temperature = 0
|
||||
maximum_survivable_temperature = INFINITY
|
||||
faction = list("demon")
|
||||
attack_verb_continuous = "wildly tears into"
|
||||
attack_verb_simple = "wildly tears into"
|
||||
maxHealth = 200
|
||||
health = 200
|
||||
environment_smash = ENVIRONMENT_SMASH_STRUCTURES
|
||||
obj_damage = 50
|
||||
melee_damage_lower = 30
|
||||
melee_damage_upper = 30
|
||||
see_in_dark = 8
|
||||
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
|
||||
basic_mob_flags = DEL_ON_DEATH
|
||||
var/datum/action/innate/demon_whisper/whisper_action
|
||||
|
||||
/mob/living/basic/demon/Initialize(mapload)
|
||||
. = ..()
|
||||
whisper_action = new()
|
||||
whisper_action.Grant(src)
|
||||
add_language("Galactic Common")
|
||||
set_default_language(GLOB.all_languages["Galactic Common"])
|
||||
|
||||
/mob/living/basic/demon/Destroy()
|
||||
QDEL_NULL(whisper_action)
|
||||
return ..()
|
||||
@@ -0,0 +1,114 @@
|
||||
// Demon heart base type
|
||||
/obj/item/organ/internal/heart/demon
|
||||
name = "demon heart"
|
||||
desc = "Still it beats furiously, emanating an aura of utter hate."
|
||||
icon_state = "demon_heart"
|
||||
origin_tech = "combat=5;biotech=7"
|
||||
organ_datums = list(/datum/organ/heart/always_beating, /datum/organ/battery)
|
||||
|
||||
/obj/item/organ/internal/heart/demon/update_icon_state()
|
||||
return //always beating visually
|
||||
|
||||
/obj/item/organ/internal/heart/demon/prepare_eat()
|
||||
return // Just so people don't accidentally waste it
|
||||
|
||||
/obj/item/organ/internal/heart/demon/attack_self__legacy__attackchain(mob/living/user)
|
||||
user.visible_message("<span class='warning'>[user] raises [src] to [user.p_their()] mouth and tears into it with [user.p_their()] teeth!</span>", \
|
||||
"<span class='danger'>An unnatural hunger consumes you. You raise [src] to your mouth and devour it!</span>")
|
||||
playsound(user, 'sound/misc/demon_consume.ogg', 50, 1)
|
||||
|
||||
/// SLAUGHTER DEMON HEART
|
||||
|
||||
/obj/item/organ/internal/heart/demon/slaughter/attack_self__legacy__attackchain(mob/living/user)
|
||||
..()
|
||||
|
||||
// Eating the heart for the first time. Gives basic bloodcrawling. This is the only time we need to insert the heart.
|
||||
if(!HAS_TRAIT(user, TRAIT_BLOODCRAWL))
|
||||
user.visible_message("<span class='warning'>[user]'s eyes flare a deep crimson!</span>", \
|
||||
"<span class='userdanger'>You feel a strange power seep into your body... you have absorbed the demon's blood-travelling powers!</span>")
|
||||
ADD_TRAIT(user, TRAIT_BLOODCRAWL, "bloodcrawl")
|
||||
user.drop_item()
|
||||
insert(user) //Consuming the heart literally replaces your heart with a demon heart. H A R D C O R E.
|
||||
return TRUE
|
||||
|
||||
// Eating a 2nd heart. Gives the ability to drag people into blood and eat them.
|
||||
if(HAS_TRAIT(user, TRAIT_BLOODCRAWL))
|
||||
to_chat(user, "You feel differ-<span class='danger'> CONSUME THEM!</span>")
|
||||
ADD_TRAIT(user, TRAIT_BLOODCRAWL_EAT, "bloodcrawl_eat")
|
||||
qdel(src) // Replacing their demon heart with another demon heart is pointless, just delete this one and return.
|
||||
return TRUE
|
||||
|
||||
// Eating any more than 2 demon hearts does nothing.
|
||||
to_chat(user, "<span class='warning'>...and you don't feel any different.</span>")
|
||||
qdel(src)
|
||||
|
||||
/obj/item/organ/internal/heart/demon/slaughter/insert(mob/living/carbon/M, special = 0)
|
||||
. = ..()
|
||||
if(M.mind)
|
||||
M.mind.AddSpell(new /datum/spell/bloodcrawl(null))
|
||||
|
||||
/obj/item/organ/internal/heart/demon/slaughter/remove(mob/living/carbon/M, special = 0)
|
||||
. = ..()
|
||||
if(M.mind)
|
||||
REMOVE_TRAIT(M, TRAIT_BLOODCRAWL, "bloodcrawl")
|
||||
REMOVE_TRAIT(M, TRAIT_BLOODCRAWL_EAT, "bloodcrawl_eat")
|
||||
M.mind.RemoveSpell(/datum/spell/bloodcrawl)
|
||||
|
||||
/// SHADOW DEMON HEART
|
||||
/obj/item/organ/internal/heart/demon/shadow
|
||||
name = "heart of darkness"
|
||||
desc = "It still beats furiously, emitting an aura of fear."
|
||||
color = COLOR_BLACK
|
||||
|
||||
/obj/item/organ/internal/heart/demon/shadow/attack_self__legacy__attackchain(mob/living/user)
|
||||
. = ..()
|
||||
user.drop_item()
|
||||
insert(user)
|
||||
|
||||
/obj/item/organ/internal/heart/demon/shadow/insert(mob/living/carbon/M, special = 0)
|
||||
. = ..()
|
||||
if(M.mind)
|
||||
M.mind.AddSpell(new /datum/spell/fireball/shadow_grapple)
|
||||
|
||||
/obj/item/organ/internal/heart/demon/shadow/remove(mob/living/carbon/M, special = 0)
|
||||
. = ..()
|
||||
if(M.mind)
|
||||
M.mind.RemoveSpell(/datum/spell/fireball/shadow_grapple)
|
||||
|
||||
/// PULSE DEMON HEART
|
||||
/obj/item/organ/internal/heart/demon/pulse
|
||||
name = "perpetual pacemaker"
|
||||
desc = "It still beats furiously, thousands of bright lights shine within it."
|
||||
color = COLOR_YELLOW
|
||||
|
||||
/obj/item/organ/internal/heart/demon/pulse/Initialize(mapload)
|
||||
. = ..()
|
||||
set_light(13, 2, "#bbbb00")
|
||||
|
||||
/obj/item/organ/internal/heart/demon/pulse/attack_self__legacy__attackchain(mob/living/user)
|
||||
. = ..()
|
||||
user.drop_item()
|
||||
insert(user)
|
||||
|
||||
/obj/item/organ/internal/heart/demon/pulse/insert(mob/living/carbon/M, special, dont_remove_slot)
|
||||
. = ..()
|
||||
M.AddComponent(/datum/component/cross_shock, 30, 500, 2 SECONDS)
|
||||
ADD_TRAIT(M, TRAIT_SHOCKIMMUNE, UNIQUE_TRAIT_SOURCE(src))
|
||||
M.set_light(3, 2, "#bbbb00")
|
||||
|
||||
/obj/item/organ/internal/heart/demon/pulse/remove(mob/living/carbon/M, special)
|
||||
. = ..()
|
||||
REMOVE_TRAIT(M, TRAIT_SHOCKIMMUNE, UNIQUE_TRAIT_SOURCE(src))
|
||||
M.remove_light()
|
||||
|
||||
/obj/item/organ/internal/heart/demon/pulse/on_life()
|
||||
if(!owner)
|
||||
return
|
||||
for(var/obj/item/stock_parts/cell/cell_to_charge in owner.GetAllContents())
|
||||
var/newcharge = min(0.05 * cell_to_charge.maxcharge + cell_to_charge.charge, cell_to_charge.maxcharge)
|
||||
if(cell_to_charge.charge < newcharge)
|
||||
cell_to_charge.charge = newcharge
|
||||
if(isobj(cell_to_charge.loc))
|
||||
var/obj/cell_location = cell_to_charge.loc
|
||||
cell_location.update_icon() //update power meters and such
|
||||
cell_to_charge.update_icon()
|
||||
@@ -0,0 +1,111 @@
|
||||
// Objective info, Based on Reverent mini Atang
|
||||
/datum/objective/slaughter
|
||||
needs_target = FALSE
|
||||
var/targetKill = 10
|
||||
|
||||
/datum/objective/slaughter/New()
|
||||
targetKill = rand(10,20)
|
||||
explanation_text = "Devour [targetKill] mortals."
|
||||
..()
|
||||
|
||||
/datum/objective/slaughter/check_completion()
|
||||
var/kill_count = 0
|
||||
for(var/datum/mind/M in get_owners())
|
||||
if(!isslaughterdemon(M.current) || QDELETED(M.current))
|
||||
continue
|
||||
var/mob/living/basic/demon/slaughter/R = M.current
|
||||
kill_count += R.devoured
|
||||
if(kill_count >= targetKill)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/objective/demon_fluff
|
||||
name = "Spread blood"
|
||||
needs_target = FALSE
|
||||
|
||||
/datum/objective/demon_fluff/New()
|
||||
find_target()
|
||||
var/targetname = "someone"
|
||||
if(target && target.current)
|
||||
targetname = target.current.real_name
|
||||
var/list/explanation_texts = list(
|
||||
"Spread blood all over the bridge.",
|
||||
"Spread blood all over the brig.",
|
||||
"Spread blood all over the chapel.",
|
||||
"Kill or Destroy all Janitors or Sanitation bots.",
|
||||
"Spare a few after striking them... make them bleed before the harvest.",
|
||||
"Hunt those that try to hunt you first.",
|
||||
"Hunt those that run away from you in fear",
|
||||
"Show [targetname] the power of blood.",
|
||||
"Drive [targetname] insane with demonic whispering."
|
||||
)
|
||||
// As this is a fluff objective, we don't need a target, so we want to null it out.
|
||||
// We don't want the demon getting a "Time for Plan B" message if the target cryos.
|
||||
target = null
|
||||
explanation_text = pick(explanation_texts)
|
||||
..()
|
||||
|
||||
/datum/objective/demon_fluff/check_completion()
|
||||
return TRUE
|
||||
|
||||
/datum/objective/cult_slaughter
|
||||
explanation_text = "Bring forth the Slaughter to the nonbelievers."
|
||||
needs_target = FALSE
|
||||
|
||||
/datum/objective/pulse_demon/infest
|
||||
name = "Hijack APCs"
|
||||
/// Amount of APCs we need to hijack, can be 15, 20, or 25
|
||||
var/amount = 0
|
||||
|
||||
/datum/objective/pulse_demon/infest/New()
|
||||
. = ..()
|
||||
amount = rand(3, 5) * 5
|
||||
explanation_text = "Hijack [amount] APCs."
|
||||
|
||||
/datum/objective/pulse_demon/infest/check_completion()
|
||||
if(..())
|
||||
return TRUE
|
||||
var/hijacked = 0
|
||||
for(var/datum/mind/M in get_owners())
|
||||
if(!ispulsedemon(M.current) || QDELETED(M.current))
|
||||
continue
|
||||
var/mob/living/basic/demon/pulse_demon/demon = M.current
|
||||
hijacked += length(demon.hijacked_apcs)
|
||||
return hijacked >= amount
|
||||
|
||||
/datum/objective/pulse_demon/drain
|
||||
name = "Drain Power"
|
||||
/// Amount of power we need to drain, ranges from 500 KW to 5 MW
|
||||
var/amount = 0
|
||||
|
||||
/datum/objective/pulse_demon/drain/New()
|
||||
. = ..()
|
||||
amount = rand(1, 10) * 500000
|
||||
explanation_text = "Drain [format_si_suffix(amount)]W of power."
|
||||
|
||||
/datum/objective/pulse_demon/drain/check_completion()
|
||||
if(..())
|
||||
return TRUE
|
||||
var/drained = 0
|
||||
for(var/datum/mind/M in get_owners())
|
||||
if(!ispulsedemon(M.current) || QDELETED(M.current))
|
||||
continue
|
||||
var/mob/living/basic/demon/pulse_demon/demon = M.current
|
||||
drained += demon.charge_drained
|
||||
return drained >= amount
|
||||
|
||||
// Requires 1 APC to be hacked and not destroyed to complete
|
||||
/datum/objective/pulse_demon/tamper
|
||||
name = "Tamper Machinery"
|
||||
explanation_text = "Cause mischief amongst the machines in rooms with APCs you've hijacked, and defend yourself from anyone trying to stop you."
|
||||
|
||||
/datum/objective/pulse_demon/tamper/check_completion()
|
||||
if(..())
|
||||
return TRUE
|
||||
for(var/datum/mind/M in get_owners())
|
||||
if(!ispulsedemon(M.current) || QDELETED(M.current))
|
||||
continue
|
||||
var/mob/living/basic/demon/pulse_demon/demon = M.current
|
||||
if(!length(demon.hijacked_apcs) || !M.active || demon.stat == DEAD)
|
||||
return FALSE
|
||||
return TRUE
|
||||
@@ -0,0 +1,685 @@
|
||||
//////////////////////////////
|
||||
// MARK: DEMONIC WHISPER
|
||||
//////////////////////////////
|
||||
/datum/action/innate/demon_whisper
|
||||
name = "Demonic Whisper"
|
||||
button_icon_state = "demon_comms"
|
||||
background_icon_state = "bg_demon"
|
||||
|
||||
/datum/action/innate/demon_whisper/proc/choose_targets(mob/user = usr)
|
||||
var/list/validtargets = list()
|
||||
for(var/mob/living/M in view(user.client.maxview(), get_turf(user)))
|
||||
if(M && M.mind && M.stat != DEAD)
|
||||
if(M == user)
|
||||
continue
|
||||
|
||||
validtargets += M
|
||||
|
||||
if(!length(validtargets))
|
||||
to_chat(usr, "<span class='warning'>There are no valid targets!</span>")
|
||||
return
|
||||
|
||||
var/mob/living/target = tgui_input_list(user, "Choose the target to talk to", "Targeting", validtargets)
|
||||
return target
|
||||
|
||||
/datum/action/innate/demon_whisper/Activate()
|
||||
var/mob/living/choice = choose_targets()
|
||||
if(!choice)
|
||||
return
|
||||
|
||||
var/msg = tgui_input_text(usr, "What do you wish to tell [choice]?", null, "")
|
||||
if(!msg)
|
||||
return
|
||||
log_say("(SLAUGHTER to [key_name(choice)]) [msg]", usr)
|
||||
to_chat(usr, "<span class='notice'><b>You whisper to [choice]: </b>[msg]</span>")
|
||||
to_chat(choice, "<span class='deadsay'><b>Suddenly a strange, demonic voice resonates in your head... </b></span><i><span class='danger'> [msg]</span></i>")
|
||||
for(var/mob/dead/observer/G in GLOB.player_list)
|
||||
G.show_message("<i>Demonic message from <b>[usr]</b> ([ghost_follow_link(usr, ghost=G)]) to <b>[choice]</b> ([ghost_follow_link(choice, ghost=G)]): [msg]</i>")
|
||||
|
||||
//////////////////////////////
|
||||
// MARK: CULT DEMON
|
||||
//////////////////////////////
|
||||
/datum/spell/sense_victims
|
||||
name = "Sense Victims"
|
||||
desc = "Sense the location of heretics."
|
||||
base_cooldown = 0
|
||||
clothes_req = FALSE
|
||||
overlay = null
|
||||
action_icon_state = "bloodcrawl"
|
||||
action_background_icon_state = "bg_cult"
|
||||
|
||||
/datum/spell/sense_victims/create_new_targeting()
|
||||
return new /datum/spell_targeting/alive_mob_list
|
||||
|
||||
/datum/spell/sense_victims/valid_target(mob/living/target, user)
|
||||
return target.stat == CONSCIOUS && target.key && !IS_CULTIST(target) // Only conscious, non cultist players
|
||||
|
||||
/datum/spell/sense_victims/cast(list/targets, mob/user)
|
||||
var/mob/living/victim = targets[1]
|
||||
to_chat(victim, "<span class='userdanger'>You feel an awful sense of being watched...</span>")
|
||||
victim.Stun(6 SECONDS) //HUE
|
||||
var/area/A = get_area(victim)
|
||||
if(!A)
|
||||
to_chat(user, "<span class='warning'>You could not locate any sapient heretics for the Slaughter.</span>")
|
||||
return 0
|
||||
to_chat(user, "<span class='danger'>You sense a terrified soul at [A]. <b>Show [A.p_them()] the error of [A.p_their()] ways.</b></span>")
|
||||
|
||||
//////////////////////////////
|
||||
// MARK: SHADOW DEMON
|
||||
//////////////////////////////
|
||||
/datum/spell/fireball/shadow_grapple
|
||||
name = "Shadow Grapple"
|
||||
desc = "Fire one of your hands, if it hits a person it pulls them in. If you hit a structure you get pulled to the structure. Any light source hit with this will be disabled in a two tile radius."
|
||||
base_cooldown = 10 SECONDS
|
||||
fireball_type = /obj/projectile/magic/shadow_hand
|
||||
|
||||
selection_activated_message = "<span class='notice'>You raise your hand, full of demonic energy! <b>Left-click to cast at a target!</b></span>"
|
||||
selection_deactivated_message = "<span class='notice'>You re-absorb the energy...for now.</span>"
|
||||
|
||||
action_background_icon_state = "shadow_demon_bg"
|
||||
action_icon_state = "shadow_grapple"
|
||||
sound = null
|
||||
invocation_type = "none"
|
||||
invocation = null
|
||||
|
||||
/datum/spell/fireball/shadow_grapple/update_spell_icon()
|
||||
return
|
||||
|
||||
/obj/projectile/magic/shadow_hand
|
||||
name = "shadow hand"
|
||||
icon_state = "shadow_hand"
|
||||
plane = FLOOR_PLANE
|
||||
hitsound = 'sound/shadowdemon/shadowattack1.ogg' // Plays when hitting something living or a light
|
||||
var/hit = FALSE
|
||||
|
||||
/obj/projectile/magic/shadow_hand/pixel_move(trajectory_multiplier, hitscanning)
|
||||
. = ..()
|
||||
var/obj/machinery/light/floor/floor_light = locate(/obj/machinery/light/floor) in get_turf(src)
|
||||
if(floor_light)
|
||||
Bump(floor_light)
|
||||
|
||||
/obj/projectile/magic/shadow_hand/fire(setAngle)
|
||||
if(firer)
|
||||
var/mob/living/basic/demon/shadow/current_demon = firer
|
||||
if(istype(current_demon))
|
||||
current_demon.block_shadow_crawl()
|
||||
firer.Beam(src, icon_state = "grabber_beam", time = INFINITY, maxdistance = INFINITY, beam_type = /obj/effect/ebeam/floor)
|
||||
return ..()
|
||||
|
||||
/obj/projectile/magic/shadow_hand/on_hit(atom/target, blocked, hit_zone)
|
||||
if(hit)
|
||||
return
|
||||
hit = TRUE // to prevent double hits from the pull
|
||||
. = ..()
|
||||
for(var/atom/extinguish_target in range(2, src))
|
||||
extinguish_target.extinguish_light(TRUE)
|
||||
if(!isliving(target))
|
||||
if(isshadowdemon(firer))
|
||||
firer.throw_at(get_step(target, get_dir(target, firer)), 50, 10, callback = CALLBACK(firer, TYPE_PROC_REF(/mob/living/basic/demon/shadow, unblock_shadow_crawl)))
|
||||
else
|
||||
firer.throw_at(get_step(target, get_dir(target, firer)), 50, 10)
|
||||
else
|
||||
unblock_shadowdemon_crawl()
|
||||
if(!.)
|
||||
return
|
||||
else
|
||||
var/mob/living/L = target
|
||||
L.Immobilize(2 SECONDS)
|
||||
L.apply_damage(40, BRUTE, BODY_ZONE_CHEST)
|
||||
L.throw_at(get_step(firer, get_dir(firer, target)), 50, 10)
|
||||
|
||||
/obj/projectile/magic/shadow_hand/Destroy()
|
||||
if(!hit)
|
||||
unblock_shadowdemon_crawl()
|
||||
return ..()
|
||||
|
||||
/obj/projectile/magic/shadow_hand/proc/unblock_shadowdemon_crawl()
|
||||
var/mob/living/basic/demon/shadow/current_demon = firer
|
||||
if(istype(current_demon))
|
||||
current_demon.unblock_shadow_crawl()
|
||||
|
||||
/obj/effect/ebeam/floor
|
||||
plane = FLOOR_PLANE
|
||||
|
||||
//////////////////////////////
|
||||
// MARK: PULSE DEMON
|
||||
//////////////////////////////
|
||||
#define PULSEDEMON_REMOTE_DRAIN_MULTIPLIER 5
|
||||
|
||||
#define PD_UPGRADE_HIJACK_SPEED "Speed"
|
||||
#define PD_UPGRADE_DRAIN_SPEED "Absorption"
|
||||
#define PD_UPGRADE_HEALTH_LOSS "Endurance"
|
||||
#define PD_UPGRADE_HEALTH_REGEN "Recovery"
|
||||
#define PD_UPGRADE_MAX_HEALTH "Strength"
|
||||
#define PD_UPGRADE_HEALTH_COST "Efficiency"
|
||||
#define PD_UPGRADE_MAX_CHARGE "Capacity"
|
||||
|
||||
/datum/spell/pulse_demon
|
||||
clothes_req = FALSE
|
||||
antimagic_flags = NONE
|
||||
action_background_icon_state = "bg_pulsedemon"
|
||||
var/locked = TRUE
|
||||
var/unlock_cost = 1
|
||||
var/cast_cost = 1 KJ
|
||||
var/upgrade_cost = 1
|
||||
var/requires_area = FALSE
|
||||
var/revealing = FALSE
|
||||
var/reveal_time = 10 SECONDS
|
||||
base_cooldown = 20 SECONDS
|
||||
|
||||
/datum/spell/pulse_demon/New()
|
||||
. = ..()
|
||||
update_info()
|
||||
|
||||
/datum/spell/pulse_demon/proc/update_info()
|
||||
if(locked)
|
||||
name = "[initial(name)] (Locked) ([format_si_suffix(unlock_cost)] APC\s)"
|
||||
desc = "[initial(desc)] It costs [format_si_suffix(unlock_cost)] APC\s to unlock. <b>Alt-Click</b> this spell to unlock it."
|
||||
else
|
||||
name = "[initial(name)][cast_cost == 0 ? "" : " ([format_si_suffix(cast_cost)]W)"]"
|
||||
desc = "[initial(desc)][spell_level == level_max ? "" : " It costs [format_si_suffix(upgrade_cost)] APC\s to upgrade. <b>Alt-Click</b> this spell to upgrade it."]"
|
||||
action.name = name
|
||||
action.desc = desc
|
||||
action.build_all_button_icons()
|
||||
|
||||
/datum/spell/pulse_demon/can_cast(mob/living/basic/demon/pulse_demon/user, charge_check, show_message)
|
||||
if(!..())
|
||||
return FALSE
|
||||
if(!istype(user))
|
||||
return FALSE
|
||||
if(locked)
|
||||
if(show_message)
|
||||
to_chat(user, "<span class='warning'>This ability is locked! Alt-click the button to purchase this ability.</span>")
|
||||
to_chat(user, "<span class='notice'>It costs [format_si_suffix(unlock_cost)] APC\s to unlock.</span>")
|
||||
return FALSE
|
||||
if(user.charge < cast_cost)
|
||||
if(show_message)
|
||||
to_chat(user, "<span class='warning'>You do not have enough charge to use this ability!</span>")
|
||||
to_chat(user, "<span class='notice'>It costs [format_si_suffix(cast_cost)]W to use.</span>")
|
||||
return FALSE
|
||||
if(requires_area && !user.controlling_area)
|
||||
if(show_message)
|
||||
to_chat(user, "<span class='warning'>You need to be controlling an area to use this ability!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/cast(list/targets, mob/living/basic/demon/pulse_demon/user)
|
||||
if(!istype(user) || locked || user.charge < cast_cost || !length(targets))
|
||||
return FALSE
|
||||
if(requires_area && !user.controlling_area)
|
||||
return FALSE
|
||||
if(requires_area && user.controlling_area != get_area(targets[1]))
|
||||
to_chat(user, "<span class='warning'>You can only use this ability in your controlled area!</span>")
|
||||
return FALSE
|
||||
if(try_cast_action(user, targets[1]))
|
||||
user.adjust_charge(-cast_cost)
|
||||
if(revealing)
|
||||
INVOKE_ASYNC(src, PROC_REF(reveal_demon), user, reveal_time)
|
||||
return TRUE
|
||||
else
|
||||
revert_cast(user)
|
||||
return FALSE
|
||||
|
||||
/datum/spell/pulse_demon/create_new_targeting()
|
||||
return new /datum/spell_targeting/clicked_atom
|
||||
|
||||
/datum/spell/pulse_demon/proc/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
return FALSE
|
||||
|
||||
/datum/spell/pulse_demon/proc/reveal_demon(mob/user, reveal_time)
|
||||
user.layer = ABOVE_NORMAL_TURF_LAYER
|
||||
do_sparks(rand(2, 4), FALSE, user)
|
||||
sleep(reveal_time)
|
||||
user.layer = ABOVE_PLATING_LAYER
|
||||
|
||||
// handles purchasing and upgrading abilities
|
||||
/datum/spell/pulse_demon/AltClick(mob/living/basic/demon/pulse_demon/user)
|
||||
if(!istype(user))
|
||||
return
|
||||
|
||||
if(locked)
|
||||
if(user.apcs_remaining >= unlock_cost)
|
||||
user.apcs_remaining -= unlock_cost
|
||||
locked = FALSE
|
||||
to_chat(user, "<span class='notice'>You have unlocked [initial(name)]!</span>")
|
||||
|
||||
if(cast_cost > 0)
|
||||
to_chat(user, "<span class='notice'>It costs [format_si_suffix(cast_cost)]W to use once.</span>")
|
||||
if(level_max > 0 && spell_level < level_max)
|
||||
to_chat(user, "<span class='notice'>It will cost [format_si_suffix(upgrade_cost)] APC\s to upgrade.</span>")
|
||||
|
||||
update_info()
|
||||
else
|
||||
to_chat(user, "<span class='warning'>You cannot afford this ability! It costs [format_si_suffix(unlock_cost)] APC\s to unlock.</span>")
|
||||
else
|
||||
if(spell_level >= level_max)
|
||||
to_chat(user, "<span class='warning'>You have already fully upgraded this ability!</span>")
|
||||
else if(user.apcs_remaining >= upgrade_cost)
|
||||
user.apcs_remaining -= upgrade_cost
|
||||
spell_level = min(spell_level + 1, level_max)
|
||||
do_upgrade(user)
|
||||
|
||||
if(spell_level == level_max)
|
||||
to_chat(user, "<span class='notice'>You have fully upgraded [initial(name)]!</span>")
|
||||
else
|
||||
to_chat(user, "<span class='notice'>The next upgrade will cost [format_si_suffix(upgrade_cost)] APC\s to unlock.</span>")
|
||||
|
||||
update_info()
|
||||
else
|
||||
to_chat(user, "<span class='warning'>You cannot afford to upgrade this ability! It costs [format_si_suffix(upgrade_cost)] APC\s to upgrade.</span>")
|
||||
|
||||
/datum/spell/pulse_demon/proc/do_upgrade(mob/living/basic/demon/pulse_demon/user)
|
||||
cooldown_handler.recharge_duration = round(base_cooldown / (1.5 ** spell_level))
|
||||
to_chat(user, "<span class='notice'>You have upgraded [initial(name)] to level [spell_level + 1], it now takes [cooldown_handler.recharge_duration / 10] seconds to recharge.</span>")
|
||||
|
||||
/datum/spell/pulse_demon/cablehop
|
||||
name = "Cable Hop"
|
||||
desc = "Jump to another cable in view."
|
||||
action_icon_state = "pd_cablehop"
|
||||
unlock_cost = 4
|
||||
cast_cost = 200 KJ
|
||||
upgrade_cost = 2
|
||||
revealing = TRUE
|
||||
reveal_time = 5 SECONDS
|
||||
|
||||
/datum/spell/pulse_demon/cablehop/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
var/turf/O = get_turf(user)
|
||||
var/turf/T = get_turf(target)
|
||||
var/obj/structure/cable/C = locate(/obj/structure/cable) in T
|
||||
if(!istype(C))
|
||||
to_chat(user, "<span class='warning'>No cable found!</span>")
|
||||
return FALSE
|
||||
if(get_dist(O, T) > 15) //Some extra range to account for them possessing machines away from their APC, but blocking demons from using a camera console to zap across the station.
|
||||
to_chat(user, "<span class='warning'>That cable is too far away!</span>")
|
||||
return FALSE
|
||||
playsound(T, 'sound/magic/lightningshock.ogg', 50, TRUE)
|
||||
O.Beam(target, icon_state = "lightning[rand(1, 12)]", icon = 'icons/effects/effects.dmi', time = 1 SECONDS)
|
||||
for(var/turf/working in get_line(O, T))
|
||||
for(var/mob/living/L in working)
|
||||
if(!electrocute_mob(L, C.powernet, user)) // give a little bit of non-lethal counterplay against insuls
|
||||
L.Jitter(5 SECONDS)
|
||||
L.apply_status_effect(STATUS_EFFECT_DELAYED, 1 SECONDS, CALLBACK(L, TYPE_PROC_REF(/mob/living, KnockDown), 5 SECONDS), COMSIG_LIVING_CLEAR_STUNS)
|
||||
user.forceMove(T)
|
||||
user.Move(T)
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/emagtamper
|
||||
name = "Electromagnetic Tamper"
|
||||
desc = "Unlocks hidden programming in machines. Must be inside a hijacked APC to use."
|
||||
action_icon_state = "pd_emag"
|
||||
unlock_cost = 4
|
||||
cast_cost = 200 KJ
|
||||
upgrade_cost = 2
|
||||
requires_area = TRUE
|
||||
revealing = TRUE
|
||||
|
||||
/datum/spell/pulse_demon/emagtamper/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
to_chat(user, "<span class='warning'>You attempt to tamper with [target]!</span>")
|
||||
target.emag_act(user)
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/emp
|
||||
name = "Electromagnetic Pulse"
|
||||
desc = "Creates an EMP where you click. Be careful not to use it on yourself!"
|
||||
action_icon_state = "pd_emp"
|
||||
unlock_cost = 4
|
||||
cast_cost = 500 KJ
|
||||
upgrade_cost = 2
|
||||
requires_area = TRUE
|
||||
revealing = TRUE
|
||||
|
||||
/datum/spell/pulse_demon/emp/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
to_chat(user, "<span class='warning'>You attempt to EMP [target]!</span>")
|
||||
empulse(get_turf(target), 1, 1)
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/overload
|
||||
name = "Overload Machine"
|
||||
desc = "Overloads a machine, causing it to explode. Becomes powerful enough to breach when fully upgraded."
|
||||
action_icon_state = "pd_overload"
|
||||
unlock_cost = 8
|
||||
cast_cost = 1000 KJ
|
||||
upgrade_cost = 4
|
||||
requires_area = TRUE
|
||||
revealing = TRUE
|
||||
base_cooldown = 60 SECONDS
|
||||
reveal_time = 20 SECONDS
|
||||
|
||||
/datum/spell/pulse_demon/overload/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
var/obj/machinery/M = target
|
||||
if(!istype(M))
|
||||
to_chat(user, "<span class='warning'>That is not a machine.</span>")
|
||||
return FALSE
|
||||
if(target.flags_2 & NO_MALF_EFFECT_2)
|
||||
to_chat(user, "<span class='warning'>That machine cannot be overloaded.</span>")
|
||||
return FALSE
|
||||
target.audible_message("<span class='italics'>You hear a loud electrical buzzing sound coming from [target]!</span>")
|
||||
addtimer(CALLBACK(src, PROC_REF(detonate), user, M), 5 SECONDS)
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/overload/proc/detonate(mob/living/basic/demon/pulse_demon/user, obj/machinery/target)
|
||||
if(!QDELETED(target))
|
||||
if(spell_level == level_max)
|
||||
explosion(get_turf(target), 0, 1, 2, 2, smoke = TRUE, cause = "Pulse Demon: [name]")
|
||||
else
|
||||
explosion(get_turf(target), 0, 0, 2, 2, smoke = TRUE, cause = "Pulse Demon: [name]")
|
||||
if(!QDELETED(target))
|
||||
qdel(target)
|
||||
|
||||
/datum/spell/pulse_demon/remotehijack
|
||||
name = "Remote Hijack"
|
||||
desc = "Remotely hijacks an APC."
|
||||
action_icon_state = "pd_remotehack"
|
||||
unlock_cost = 2
|
||||
cast_cost = 200 KJ
|
||||
level_max = 0
|
||||
base_cooldown = 3 SECONDS // you have to wait for the regular hijack time anyway
|
||||
|
||||
/datum/spell/pulse_demon/remotehijack/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
var/obj/machinery/power/apc/A = target
|
||||
if(!istype(A))
|
||||
to_chat(user, "<span class='warning'>That is not an APC.</span>")
|
||||
return FALSE
|
||||
if(!user.try_hijack_apc(A, TRUE))
|
||||
to_chat(user, "<span class='warning'>You cannot hijack that APC right now!</span>")
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/remotedrain
|
||||
name = "Remote Drain"
|
||||
desc = "Remotely drains a power source."
|
||||
action_icon_state = "pd_remotedrain"
|
||||
unlock_cost = 2
|
||||
cast_cost = 50 KJ
|
||||
upgrade_cost = 2
|
||||
|
||||
/datum/spell/pulse_demon/remotedrain/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
if(isapc(target))
|
||||
var/drained = user.drain_APC(target, PULSEDEMON_REMOTE_DRAIN_MULTIPLIER)
|
||||
if(drained == PULSEDEMON_SOURCE_DRAIN_INVALID)
|
||||
to_chat(user, "<span class='warning'>This APC is being hijacked, you cannot drain from it right now.</span>")
|
||||
else
|
||||
to_chat(user, "<span class='notice'>You drain [format_si_suffix(drained)]W from [target].</span>")
|
||||
else if(istype(target, /obj/machinery/power/smes))
|
||||
var/drained = user.drain_SMES(target, PULSEDEMON_REMOTE_DRAIN_MULTIPLIER)
|
||||
to_chat(user, "<span class='notice'>You drain [format_si_suffix(drained)]W from [target].</span>")
|
||||
else
|
||||
to_chat(user, "<span class='warning'>That is not a valid source.</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/toggle
|
||||
base_cooldown = 0
|
||||
cast_cost = 0
|
||||
create_attack_logs = FALSE
|
||||
var/base_message = "see messages you shouldn't!"
|
||||
|
||||
/datum/spell/pulse_demon/toggle/New(initstate = FALSE)
|
||||
. = ..()
|
||||
do_toggle(initstate, null)
|
||||
|
||||
/datum/spell/pulse_demon/toggle/create_new_targeting()
|
||||
return new /datum/spell_targeting/self
|
||||
|
||||
/datum/spell/pulse_demon/toggle/proc/do_toggle(varstate, mob/user)
|
||||
if(action)
|
||||
action.background_icon_state = varstate ? action_background_icon_state : "[action_background_icon_state]_disabled"
|
||||
action.build_all_button_icons()
|
||||
if(user)
|
||||
to_chat(user, "<span class='notice'>You will [varstate ? "now" : "no longer"] [base_message]</span>")
|
||||
return varstate
|
||||
|
||||
/datum/spell/pulse_demon/toggle/do_drain
|
||||
name = "Toggle Draining"
|
||||
desc = "Toggle whether you drain charge from power sources."
|
||||
base_message = "drain charge from power sources."
|
||||
action_icon_state = "pd_toggle_steal"
|
||||
locked = FALSE
|
||||
level_max = 0
|
||||
|
||||
/datum/spell/pulse_demon/toggle/do_drain/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
user.do_drain = do_toggle(!user.do_drain, user)
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/toggle/do_drain/AltClick(mob/living/basic/demon/pulse_demon/user)
|
||||
if(!istype(user))
|
||||
return
|
||||
|
||||
var/amount = text2num(input(user, "Input a value between 1 and [user.max_drain_rate]. 0 will reset it to the maximum.", "Drain Speed Setting"))
|
||||
if(amount == null || amount < 0)
|
||||
to_chat(user, "<span class='warning'>Invalid input. Drain speed has not been modified.</span>")
|
||||
return
|
||||
|
||||
if(amount == 0)
|
||||
amount = user.max_drain_rate
|
||||
user.power_drain_rate = amount
|
||||
to_chat(user, "<span class='notice'>Drain speed has been set to [format_si_suffix(user.power_drain_rate)]W per second.</span>")
|
||||
|
||||
/datum/spell/pulse_demon/toggle/can_exit_cable
|
||||
name = "Toggle Self-Sustaining"
|
||||
desc = "Toggle whether you can move outside of cables or power sources."
|
||||
base_message = "move outside of cables."
|
||||
action_icon_state = "pd_toggle_exit"
|
||||
upgrade_cost = 2
|
||||
level_max = 3
|
||||
locked = FALSE
|
||||
|
||||
/datum/spell/pulse_demon/toggle/can_exit_cable/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
if(user.can_exit_cable && !(user.current_cable || user.current_power))
|
||||
to_chat(user, "<span class='warning'>Enter a cable or power source first!</span>")
|
||||
return FALSE
|
||||
user.can_exit_cable = do_toggle(!user.can_exit_cable, user)
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/toggle/can_exit_cable/do_upgrade(mob/living/basic/demon/pulse_demon/user)
|
||||
user.outside_cable_speed = max(initial(user.outside_cable_speed) - spell_level, 1)
|
||||
to_chat(user, "<span class='notice'>You have upgraded [initial(name)] to level [spell_level + 1], you will now move faster outside of cables.</span>")
|
||||
|
||||
/datum/spell/pulse_demon/cycle_camera
|
||||
name = "Cycle Camera View"
|
||||
desc = "Jump between the cameras in your APC's area. Alt-click to return to the APC."
|
||||
action_icon_state = "pd_camera_view"
|
||||
create_attack_logs = FALSE
|
||||
locked = FALSE
|
||||
cast_cost = 0
|
||||
level_max = 0
|
||||
base_cooldown = 0
|
||||
requires_area = TRUE
|
||||
var/current_camera = 0
|
||||
|
||||
/datum/spell/pulse_demon/cycle_camera/create_new_targeting()
|
||||
return new /datum/spell_targeting/self
|
||||
|
||||
/datum/spell/pulse_demon/cycle_camera/AltClick(mob/living/basic/demon/pulse_demon/user)
|
||||
if(!istype(user))
|
||||
return
|
||||
current_camera = 0
|
||||
|
||||
if(!isapc(user.current_power))
|
||||
return
|
||||
if(get_area(user.loc) != user.controlling_area)
|
||||
return
|
||||
user.forceMove(user.current_power)
|
||||
|
||||
/datum/spell/pulse_demon/cycle_camera/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
if(!length(user.controlling_area.cameras))
|
||||
return FALSE
|
||||
|
||||
if(isapc(user.loc))
|
||||
current_camera = 0
|
||||
else if(istype(user.loc, /obj/machinery/camera))
|
||||
current_camera = (current_camera + 1) % length(user.controlling_area.cameras)
|
||||
if(current_camera == 0)
|
||||
user.forceMove(user.current_power)
|
||||
return TRUE
|
||||
|
||||
if(length(user.controlling_area.cameras) < current_camera)
|
||||
current_camera = 0
|
||||
|
||||
user.forceMove(locateUID(user.controlling_area.cameras[current_camera + 1]))
|
||||
return TRUE
|
||||
|
||||
/datum/spell/pulse_demon/toggle/penetrating_shock
|
||||
name = "Toggle Intense Shocks"
|
||||
desc = "Toggle whether to use 200 KJ of energy to bypass electric-resistant victims immunity when attacking."
|
||||
base_message = "use strong shocks when attacking."
|
||||
action_icon_state = "pd_strong_shocks"
|
||||
unlock_cost = 4
|
||||
level_max = 0
|
||||
|
||||
/datum/spell/pulse_demon/toggle/penetrating_shock/create_new_targeting()
|
||||
return new /datum/spell_targeting/self
|
||||
|
||||
/datum/spell/pulse_demon/toggle/penetrating_shock/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
user.strong_shocks = do_toggle(!user.strong_shocks, user)
|
||||
|
||||
/datum/spell/pulse_demon/open_upgrades
|
||||
name = "Open Upgrade Menu"
|
||||
desc = "Open the upgrades menu. Alt-click for descriptions and costs."
|
||||
action_icon_state = "pd_upgrade"
|
||||
create_attack_logs = FALSE
|
||||
locked = FALSE
|
||||
cast_cost = 0
|
||||
level_max = 0
|
||||
base_cooldown = 0
|
||||
var/static/list/upgrade_icons = list(
|
||||
PD_UPGRADE_HIJACK_SPEED = image(icon = 'icons/obj/power.dmi', icon_state = "apcemag"),
|
||||
PD_UPGRADE_DRAIN_SPEED = image(icon = 'icons/obj/power.dmi', icon_state = "ccharger"),
|
||||
PD_UPGRADE_MAX_HEALTH = image(icon = 'icons/obj/stock_parts.dmi', icon_state = "bluespace_matter_bin"),
|
||||
PD_UPGRADE_HEALTH_REGEN = image(icon = 'icons/obj/stock_parts.dmi', icon_state = "femto_mani"),
|
||||
PD_UPGRADE_HEALTH_LOSS = image(icon = 'icons/obj/stock_parts.dmi', icon_state = "triphasic_scan_module"),
|
||||
PD_UPGRADE_HEALTH_COST = image(icon = 'icons/obj/stock_parts.dmi', icon_state = "quadultra_micro_laser"),
|
||||
PD_UPGRADE_MAX_CHARGE = image(icon = 'icons/obj/stock_parts.dmi', icon_state = "quadratic_capacitor")
|
||||
)
|
||||
var/static/list/upgrade_descs = list(
|
||||
PD_UPGRADE_HIJACK_SPEED = "Decrease the amount of time required to hijack an APC.",
|
||||
PD_UPGRADE_DRAIN_SPEED = "Increase the amount of charge drained from a power source per cycle.",
|
||||
PD_UPGRADE_MAX_HEALTH = "Increase the total amount of health you can have at once.",
|
||||
PD_UPGRADE_HEALTH_REGEN = "Increase the amount of health regenerated when powered per cycle.",
|
||||
PD_UPGRADE_HEALTH_LOSS = "Decrease the amount of health lost when unpowered per cycle.",
|
||||
PD_UPGRADE_HEALTH_COST = "Decrease the amount of power required to regenerate per cycle.",
|
||||
PD_UPGRADE_MAX_CHARGE = "Increase the total amount of charge you can have at once."
|
||||
)
|
||||
|
||||
/datum/spell/pulse_demon/open_upgrades/create_new_targeting()
|
||||
return new /datum/spell_targeting/self
|
||||
|
||||
/datum/spell/pulse_demon/open_upgrades/proc/calc_cost(mob/living/basic/demon/pulse_demon/user, upgrade)
|
||||
var/cost
|
||||
switch(upgrade)
|
||||
if(PD_UPGRADE_HIJACK_SPEED)
|
||||
if(user.hijack_time <= 6 SECONDS)
|
||||
return -1
|
||||
if(user.hijack_time > 12 SECONDS)
|
||||
cost = 1
|
||||
else
|
||||
cost = 2
|
||||
if(PD_UPGRADE_DRAIN_SPEED)
|
||||
if(user.max_drain_rate >= 500 KJ)
|
||||
return -1
|
||||
cost = 1
|
||||
if(PD_UPGRADE_MAX_HEALTH)
|
||||
if(user.maxHealth >= 200)
|
||||
return -1
|
||||
if(user.maxHealth <= 100)
|
||||
cost = 1
|
||||
else
|
||||
cost = 2
|
||||
if(PD_UPGRADE_HEALTH_REGEN)
|
||||
if(user.health_regen_rate >= 10)
|
||||
return -1
|
||||
cost = 1
|
||||
if(PD_UPGRADE_HEALTH_LOSS)
|
||||
if(user.health_loss_rate <= 0)
|
||||
return -1
|
||||
if(user.health_loss_rate >= 3)
|
||||
cost = 1
|
||||
else
|
||||
cost = 2
|
||||
if(PD_UPGRADE_HEALTH_COST)
|
||||
if(user.power_per_regen <= 1 KJ)
|
||||
return -1
|
||||
cost = 1
|
||||
if(PD_UPGRADE_MAX_CHARGE)
|
||||
cost = 1
|
||||
else
|
||||
return -1
|
||||
return cost
|
||||
|
||||
/datum/spell/pulse_demon/open_upgrades/proc/get_upgrades(mob/living/basic/demon/pulse_demon/user)
|
||||
var/upgrades = list()
|
||||
for(var/upgrade in upgrade_icons)
|
||||
var/cost = calc_cost(user, upgrade)
|
||||
if(cost == -1)
|
||||
continue
|
||||
upgrades["[upgrade] ([format_si_suffix(cost)] APC\s)"] = upgrade_icons[upgrade]
|
||||
return upgrades
|
||||
|
||||
/datum/spell/pulse_demon/open_upgrades/AltClick(mob/living/basic/demon/pulse_demon/user)
|
||||
if(!istype(user))
|
||||
return
|
||||
|
||||
to_chat(user, "<b>Pulse Demon upgrades:</b>")
|
||||
for(var/upgrade in upgrade_descs)
|
||||
var/cost = calc_cost(user, upgrade)
|
||||
to_chat(user, "<b>[upgrade]</b> ([cost == -1 ? "Fully Upgraded" : "[format_si_suffix(cost)]J"]) - [upgrade_descs[upgrade]]")
|
||||
|
||||
/datum/spell/pulse_demon/open_upgrades/try_cast_action(mob/living/basic/demon/pulse_demon/user, atom/target)
|
||||
var/upgrades = get_upgrades(user)
|
||||
if(!length(upgrades))
|
||||
to_chat(user, "<span class='warning'>You have already fully upgraded everything available!</span>")
|
||||
return FALSE
|
||||
|
||||
var/raw_choice = show_radial_menu(user, user, upgrades, radius = 48)
|
||||
if(!raw_choice)
|
||||
return
|
||||
var/choice = splittext(raw_choice, " ")[1]
|
||||
|
||||
var/cost = calc_cost(user, choice)
|
||||
if(cost == -1)
|
||||
return FALSE
|
||||
if(user.apcs_remaining < cost)
|
||||
to_chat(user, "<span class='warning'>You do not have enough unused APCs to purchase this upgrade!</span>")
|
||||
return FALSE
|
||||
|
||||
user.apcs_remaining -= cost
|
||||
switch(choice)
|
||||
if(PD_UPGRADE_HIJACK_SPEED)
|
||||
user.hijack_time = max(round(user.hijack_time - 3 SECONDS), 6 SECONDS)
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], it now takes [user.hijack_time / (1 SECONDS)] second\s to hijack APCs.</span>")
|
||||
if(PD_UPGRADE_DRAIN_SPEED)
|
||||
var/old = user.max_drain_rate
|
||||
user.max_drain_rate = user.max_drain_rate + 20 KJ
|
||||
if(user.power_drain_rate == old)
|
||||
user.power_drain_rate = user.max_drain_rate
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], you can now drain [format_si_suffix(user.max_drain_rate)]W.</span>")
|
||||
if(PD_UPGRADE_MAX_HEALTH)
|
||||
user.maxHealth = min(round(user.maxHealth * 1.5), 200)
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], your max health is now [user.maxHealth].</span>")
|
||||
if(PD_UPGRADE_HEALTH_REGEN)
|
||||
user.health_regen_rate = min(round(user.health_regen_rate + 2), 10)
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], you will now regenerate [user.health_regen_rate] health per cycle when powered.</span>")
|
||||
if(PD_UPGRADE_HEALTH_LOSS)
|
||||
user.health_loss_rate = max(round(user.health_loss_rate - 1), 0)
|
||||
if(user.health_loss_rate == 0)
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], You will no longer lose health when on an unpowered cable.</span>")
|
||||
else
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], you will now lose [user.health_loss_rate] health per cycle when unpowered.</span>")
|
||||
if(PD_UPGRADE_HEALTH_COST)
|
||||
user.power_per_regen = max(round(user.power_per_regen / 1.5), 1)
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], it now takes [format_si_suffix(user.power_per_regen)]W of power to regenerate health.</span>")
|
||||
to_chat(user, "<span class='notice'>Additionally, if you enable draining while on a cable, any excess power that would've been used regenerating will be added to your charge.</span>")
|
||||
if(PD_UPGRADE_MAX_CHARGE)
|
||||
user.maxcharge = user.maxcharge + 50 KJ
|
||||
to_chat(user, "<span class='notice'>You have upgraded your [choice], you can now store [format_si_suffix(user.maxcharge)]J of energy.</span>")
|
||||
else
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
#undef PULSEDEMON_REMOTE_DRAIN_MULTIPLIER
|
||||
#undef PD_UPGRADE_HIJACK_SPEED
|
||||
#undef PD_UPGRADE_DRAIN_SPEED
|
||||
#undef PD_UPGRADE_HEALTH_LOSS
|
||||
#undef PD_UPGRADE_HEALTH_REGEN
|
||||
#undef PD_UPGRADE_MAX_HEALTH
|
||||
#undef PD_UPGRADE_HEALTH_COST
|
||||
#undef PD_UPGRADE_MAX_CHARGE
|
||||
@@ -0,0 +1,909 @@
|
||||
// original implementation: https://ss13.moe/wiki/index.php/Pulse_Demon
|
||||
|
||||
|
||||
|
||||
#define PULSEDEMON_PLATING_SPARK_CHANCE 20
|
||||
#define PULSEDEMON_APC_CHARGE_MULTIPLIER 2
|
||||
#define PULSEDEMON_SMES_DRAIN_MULTIPLIER 10
|
||||
#define ALERT_CATEGORY_NOPOWER "pulse_nopower"
|
||||
#define ALERT_CATEGORY_NOREGEN "pulse_noregen"
|
||||
/// Conversion ratio from Watt ticks to joules.
|
||||
|
||||
/mob/living/basic/demon/pulse_demon
|
||||
name = "pulse demon"
|
||||
real_name = "pulse demon"
|
||||
desc = "A strange electrical apparition that lives in wires."
|
||||
|
||||
damage_coeff = list(BRUTE = 0.25, BURN = 0.5, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) // Pulse demons take reduced damage from most sources. Use ions
|
||||
|
||||
speak_emote = list("modulates", "vibrates", "sizzles")
|
||||
|
||||
icon_state = "pulsedem"
|
||||
icon_living = "pulsedem"
|
||||
icon_dead = "pulsedem"
|
||||
response_help_continuous = "reaches their hand into"
|
||||
response_help_simple = "reach their hand into"
|
||||
response_disarm_continuous = "pushes their hand through"
|
||||
response_disarm_simple = "push their hand through"
|
||||
response_harm_continuous = "punches their fist through"
|
||||
response_harm_simple = "punches their fist through"
|
||||
death_message = "fizzles out into faint sparks, leaving only a slight trail of smoke..."
|
||||
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) // "I don't need air, just MORE POWER!"
|
||||
unsuitable_atmos_damage = 0
|
||||
level = 1
|
||||
plane = FLOOR_PLANE
|
||||
layer = ABOVE_PLATING_LAYER
|
||||
|
||||
maxHealth = 50
|
||||
health = 50
|
||||
speed = -0.3
|
||||
mob_size = MOB_SIZE_TINY
|
||||
density = FALSE
|
||||
|
||||
attack_verb_continuous = "electrocutes"
|
||||
attack_verb_simple = "electrocute"
|
||||
attack_sound = "sparks"
|
||||
harm_intent_damage = 0
|
||||
melee_damage_lower = 0
|
||||
melee_damage_upper = 0
|
||||
pass_flags = PASSDOOR
|
||||
has_unlimited_silicon_privilege = TRUE
|
||||
// this makes the demon able to speak through holopads, due to the overriden say, PD cannot speak normally regardless
|
||||
universal_speak = TRUE
|
||||
loot = list(/obj/item/organ/internal/heart/demon/pulse)
|
||||
initial_traits = list(TRAIT_FLYING)
|
||||
|
||||
/// List of sounds that is picked from when the demon speaks.
|
||||
var/list/speech_sounds = list("sound/voice/pdvoice1.ogg", "sound/voice/pdvoice2.ogg", "sound/voice/pdvoice3.ogg")
|
||||
/// List of sounds that is picked from when the demon dies or is EMP'd.
|
||||
var/list/hurt_sounds = list("sound/voice/pdwail1.ogg", "sound/voice/pdwail2.ogg", "sound/voice/pdwail3.ogg")
|
||||
|
||||
/// Current quantity of energy the demon currently holds (Joules), spent while purchasing, upgrading or using spells or upgrades. Use adjust_charge to modify this.
|
||||
var/charge = 100 KJ
|
||||
/// Maximum quantity of energy the demon can hold at once (Joules).
|
||||
var/maxcharge = 100 KJ
|
||||
/// Book keeping for objective win conditions (Joules).
|
||||
var/charge_drained = 0
|
||||
/// Controls whether the demon will drain power from sources. Toggled by a spell.
|
||||
var/do_drain = TRUE
|
||||
/// Amount of power (Watts) to drain from power sources every Life tick.
|
||||
var/power_drain_rate = 20 KJ
|
||||
/// Maximum value for power_drain_rate based on upgrades. (Watts)
|
||||
var/max_drain_rate = 100 KJ
|
||||
|
||||
/// Amount of power (Watts) required to regenerate health.
|
||||
var/power_per_regen = 200 KJ
|
||||
/// Amount of health lost per Life tick when the power requirement was not met.
|
||||
var/health_loss_rate = 5
|
||||
/// Amount of health regenerated per Life tick when the power requirement was met.
|
||||
var/health_regen_rate = 2
|
||||
/// Lock health regeneration while this is not 0, decreases by 1 every Life tick.
|
||||
var/regen_lock = 0
|
||||
/// Tracking to prevent multiple EMPs in the same tick from instakilling a demon.
|
||||
var/emp_debounce = FALSE
|
||||
|
||||
/// Controls whether the demon can move outside of cables. Toggled by a spell.
|
||||
var/can_exit_cable = FALSE
|
||||
/// Speed used while moving inside cables.
|
||||
var/inside_cable_speed = -0.3
|
||||
/// Speed used while moving outside cables. Can be upgraded.
|
||||
var/outside_cable_speed = 5
|
||||
|
||||
/// The time it takes to hijack APCs and cyborgs.
|
||||
var/hijack_time = 20 SECONDS
|
||||
|
||||
/// The color of light the demon emits. The range of the light is proportional to energy stored.
|
||||
var/glow_color = "#bbbb00"
|
||||
|
||||
/// Area being controlled, should be maintained as long as the demon does not move outside a container (APC, object, robot, bot).
|
||||
var/area/controlling_area
|
||||
/// Inhabited cable, only maintained while on top of the cable.
|
||||
var/obj/structure/cable/current_cable
|
||||
/// Inhabited power source, maintained while inside, or while inside its area if it is an APC.
|
||||
var/obj/machinery/power/current_power
|
||||
/// Inhabited item, only items which can be used in rechargers can be hijacked. Only maintained while inside the item.
|
||||
var/obj/item/current_weapon
|
||||
/// Inhabited cyborg, only maintained while inside the cyborg.
|
||||
var/mob/living/silicon/robot/current_robot
|
||||
/// Inhabited bot, only maintained while inside the bot.
|
||||
var/mob/living/simple_animal/bot/current_bot
|
||||
|
||||
/// Delay tracker for movement inside bots.
|
||||
var/bot_movedelay = 0
|
||||
/// A cyborg that has already been hijacked can be re-entered instantly.
|
||||
var/list/hijacked_robots = list()
|
||||
|
||||
/// Images of cables currently being shown on the client.
|
||||
var/list/cable_images = list()
|
||||
/// Images of APCs currently being shown on the client.
|
||||
var/list/apc_images = list()
|
||||
/// List of all previously hijacked APCs.
|
||||
var/list/hijacked_apcs = list()
|
||||
/// Current APC amount for use as upgrade currency
|
||||
var/list/apcs_remaining = 0
|
||||
/// Does the pulse demon attempt to use internal power to bypass shock immunity
|
||||
var/strong_shocks = FALSE
|
||||
/// Reference to the APC currently being hijacked.
|
||||
var/obj/machinery/power/apc/apc_being_hijacked
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/wizard
|
||||
name = "Empowered Pulse Demon"
|
||||
real_name = "Empowered Pulse Demon"
|
||||
desc = "A strange electrical apparition that lives in wires. This one appears more charged than usual."
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/wizard/Initialize(mapload)
|
||||
. = ..()
|
||||
src.apcs_remaining = 20
|
||||
src.maxcharge = 500 KJ
|
||||
src.charge = 100 KJ
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/Initialize(mapload)
|
||||
. = ..()
|
||||
if(!mapload)
|
||||
name += " ([rand(100, 999)])"
|
||||
real_name = name
|
||||
|
||||
remove_from_all_data_huds()
|
||||
ADD_TRAIT(src, TRAIT_AI_UNTRACKABLE, PULSEDEMON_TRAIT)
|
||||
flags_2 |= RAD_NO_CONTAMINATE_2
|
||||
|
||||
// For when someone steps on us
|
||||
var/static/list/loc_connections = list(
|
||||
COMSIG_ATOM_ENTERED = PROC_REF(on_atom_entered)
|
||||
)
|
||||
AddElement(/datum/element/connect_loc, loc_connections)
|
||||
// For when we move somewhere else
|
||||
RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_movable_moved))
|
||||
|
||||
// drop demon onto ground if its loc is a non-turf and gets deleted
|
||||
RegisterSignal(src, COMSIG_PARENT_PREQDELETED, PROC_REF(deleted_handler))
|
||||
|
||||
RegisterSignal(SSdcs, COMSIG_GLOB_CABLE_UPDATED, PROC_REF(cable_updated_handler))
|
||||
|
||||
RegisterSignal(src, COMSIG_BODY_TRANSFER_TO, PROC_REF(make_pulse_antagonist))
|
||||
RegisterSignal(src, COMSIG_ATOM_EMP_ACT, PROC_REF(handle_emp))
|
||||
|
||||
current_power = locate(/obj/machinery/power) in loc
|
||||
// in the case that both current_power and current_cable are null, the pulsedemon will die the next tick
|
||||
if(!current_power)
|
||||
current_cable = locate(/obj/structure/cable) in loc
|
||||
else
|
||||
forceMove(current_power)
|
||||
update_glow()
|
||||
playsound(get_turf(src), 'sound/effects/eleczap.ogg', 30, TRUE)
|
||||
give_spells()
|
||||
whisper_action.button_icon_state = "pulse_whisper"
|
||||
whisper_action.background_icon_state = "bg_pulsedemon"
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/deleted_handler(our_demon, force)
|
||||
SIGNAL_HANDLER
|
||||
// assume normal deletion if we're on a turf, otherwise deletion could be inherited from loc
|
||||
if(force || isnull(loc) || isturf(loc))
|
||||
return FALSE
|
||||
if(!(basic_mob_flags & DEL_ON_DEATH))
|
||||
return FALSE
|
||||
exit_to_turf()
|
||||
return TRUE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/cable_updated_handler(SSdcs, turf/T)
|
||||
SIGNAL_HANDLER
|
||||
if(cable_images[T])
|
||||
var/list/turf_images = cable_images[T]
|
||||
for(var/image/current_image in turf_images)
|
||||
client?.images -= current_image
|
||||
turf_images.Cut()
|
||||
else
|
||||
cable_images[T] = list()
|
||||
|
||||
for(var/obj/structure/cable/C in T)
|
||||
var/image/cable_image = image(C, C, layer = ABOVE_LIGHTING_LAYER, dir = C.dir)
|
||||
cable_image.plane = ABOVE_LIGHTING_PLANE
|
||||
cable_images[T] += cable_image
|
||||
client?.images += cable_image
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/apc_deleted_handler(obj/machinery/power/apc/A, force)
|
||||
SIGNAL_HANDLER
|
||||
hijacked_apcs -= A
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/Destroy()
|
||||
cable_images.Cut()
|
||||
apc_images.Cut()
|
||||
|
||||
controlling_area = null
|
||||
current_bot = null
|
||||
current_cable = null
|
||||
current_power = null
|
||||
current_robot = null
|
||||
current_weapon = null
|
||||
apc_being_hijacked = null
|
||||
hijacked_apcs = null
|
||||
hijacked_robots = null
|
||||
|
||||
return ..()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/Login()
|
||||
. = ..()
|
||||
update_cableview()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/make_pulse_antagonist(demon)
|
||||
SIGNAL_HANDLER
|
||||
mind.assigned_role = SPECIAL_ROLE_DEMON
|
||||
mind.special_role = SPECIAL_ROLE_DEMON
|
||||
give_objectives()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/vv_edit_var(var_name, var_value)
|
||||
switch(var_name)
|
||||
if("glow_color")
|
||||
update_glow()
|
||||
if("charge")
|
||||
// automatically adjusts maxcharge to allow the new value
|
||||
adjust_charge(var_value - charge, TRUE)
|
||||
return TRUE
|
||||
return ..()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/forceMove(atom/destination)
|
||||
var/old_location = loc
|
||||
. = ..()
|
||||
current_weapon = null
|
||||
current_robot = null
|
||||
if(current_bot)
|
||||
current_bot.hijacked = FALSE
|
||||
current_bot = null
|
||||
if(istype(old_location, /obj/item/stock_parts/cell))
|
||||
var/obj/item/stock_parts/cell/C = old_location
|
||||
// only set rigged if there are no remaining demons in the cell
|
||||
C.rigged = !(locate(/mob/living/basic/demon/pulse_demon) in old_location)
|
||||
if(istype(loc, /obj/item/stock_parts/cell))
|
||||
var/obj/item/stock_parts/cell/C = loc
|
||||
C.rigged = FALSE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/give_objectives()
|
||||
if(!mind)
|
||||
return
|
||||
mind.wipe_memory()
|
||||
var/list/greeting = list()
|
||||
greeting.Add("<span class='warning'><font size=3><b>You are a pulse demon.</b></font></span>")
|
||||
greeting.Add("<b>A being made of pure electrical energy, you travel through the station's wires and infest machinery.</b>")
|
||||
greeting.Add("<b>Navigate the station's power cables to find power sources to steal from to power your abilities.</b>")
|
||||
greeting.Add("<b>Hijack APCs by entering them to unlock more powerful abilities, gain more maximum charge, and gain access to connected machines.</b>")
|
||||
greeting.Add("<b>If the wire or power source you're connected to runs out of power you'll start losing health and eventually die, but you are otherwise resistant to damage.</b>")
|
||||
greeting.Add("<span class='motd'>For more information, check the wiki page: ([GLOB.configuration.url.wiki_url]/index.php/Pulse_Demon)</span>")
|
||||
for(var/datum/objective/new_obj in list(/datum/objective/pulse_demon/infest, /datum/objective/pulse_demon/drain, /datum/objective/pulse_demon/tamper))
|
||||
mind.add_mind_objective(new_obj)
|
||||
greeting.Add(mind.prepare_announce_objectives(FALSE))
|
||||
to_chat(src, chat_box_red(greeting.Join("<br>")))
|
||||
SSticker.mode.traitors |= mind
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/give_spells()
|
||||
AddSpell(new /datum/spell/pulse_demon/open_upgrades)
|
||||
AddSpell(new /datum/spell/pulse_demon/cycle_camera)
|
||||
AddSpell(new /datum/spell/pulse_demon/toggle/penetrating_shock(strong_shocks))
|
||||
AddSpell(new /datum/spell/pulse_demon/toggle/do_drain(do_drain))
|
||||
AddSpell(new /datum/spell/pulse_demon/toggle/can_exit_cable(can_exit_cable))
|
||||
AddSpell(new /datum/spell/pulse_demon/cablehop)
|
||||
AddSpell(new /datum/spell/pulse_demon/emagtamper)
|
||||
AddSpell(new /datum/spell/pulse_demon/emp)
|
||||
AddSpell(new /datum/spell/pulse_demon/overload)
|
||||
AddSpell(new /datum/spell/pulse_demon/remotehijack)
|
||||
AddSpell(new /datum/spell/pulse_demon/remotedrain)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/get_status_tab_items()
|
||||
var/list/status_tab_data = ..()
|
||||
. = status_tab_data
|
||||
status_tab_data[++status_tab_data.len] = list("Energy:", "[format_si_suffix(charge)]J")
|
||||
status_tab_data[++status_tab_data.len] = list("Maximum Energy:", "[format_si_suffix(maxcharge)]J")
|
||||
status_tab_data[++status_tab_data.len] = list("Drained Energy:", "[format_si_suffix(charge_drained)]J")
|
||||
status_tab_data[++status_tab_data.len] = list("Hijacked APCs:", "[length(hijacked_apcs)]")
|
||||
status_tab_data[++status_tab_data.len] = list("Unused APCs:", "[apcs_remaining]")
|
||||
status_tab_data[++status_tab_data.len] = list("Drain Rate:", "[format_si_suffix(power_drain_rate)]W")
|
||||
status_tab_data[++status_tab_data.len] = list("Hijack Time:", "[hijack_time / 10] seconds")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/dust()
|
||||
return death()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/gib()
|
||||
return death()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/death()
|
||||
var/turf/T = get_turf(src)
|
||||
do_sparks(rand(2, 4), FALSE, src)
|
||||
. = ..()
|
||||
|
||||
var/heavy_radius = min(charge / 50000, 20)
|
||||
var/light_radius = min(charge / 25000, 25)
|
||||
empulse(T, heavy_radius, light_radius)
|
||||
playsound(T, pick(hurt_sounds), 30, TRUE)
|
||||
SEND_SIGNAL(src, COMSIG_MOB_DEATH)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/exit_to_turf()
|
||||
var/turf/T = get_turf(src)
|
||||
current_power = null
|
||||
update_controlling_area()
|
||||
current_cable = null
|
||||
forceMove(T)
|
||||
Move(T)
|
||||
if(!current_cable && !current_power)
|
||||
var/datum/spell/pulse_demon/toggle/can_exit_cable/S = locate() in mob_spell_list
|
||||
if(!S.locked && !can_exit_cable)
|
||||
can_exit_cable = TRUE
|
||||
S.do_toggle(can_exit_cable)
|
||||
to_chat(src, "<span class='danger'>Your self-sustaining ability has automatically enabled itself to prevent death from having no connection!</span>")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/update_controlling_area(reset = FALSE)
|
||||
var/area/prev = controlling_area
|
||||
if(reset || current_power == null)
|
||||
controlling_area = null
|
||||
else if(isapc(current_power))
|
||||
var/obj/machinery/power/apc/A = current_power
|
||||
if(A in hijacked_apcs)
|
||||
controlling_area = A.apc_area
|
||||
else
|
||||
controlling_area = null
|
||||
|
||||
if((!prev && !controlling_area) || (prev && controlling_area))
|
||||
return // only update icons when we get or no longer have ANY area
|
||||
for(var/datum/spell/pulse_demon/S in mob_spell_list)
|
||||
if(!S.action || S.locked)
|
||||
continue
|
||||
if(S.requires_area)
|
||||
S.action.build_all_button_icons()
|
||||
|
||||
// can enter an apc at all?
|
||||
/mob/living/basic/demon/pulse_demon/proc/is_valid_apc(obj/machinery/power/apc/A)
|
||||
return istype(A) && !(A.stat & BROKEN) && !A.shorted
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/Move(newloc)
|
||||
var/obj/machinery/power/new_power = locate(/obj/machinery/power) in newloc
|
||||
var/obj/structure/cable/new_cable = locate(/obj/structure/cable) in newloc
|
||||
|
||||
if(QDELETED(new_power))
|
||||
new_power = null
|
||||
if(QDELETED(new_cable))
|
||||
new_cable = null
|
||||
|
||||
if(istype(new_power, /obj/machinery/power/terminal))
|
||||
// entering a terminal is kinda useless and any working terminal will have a cable under it
|
||||
new_power = null
|
||||
|
||||
if(isapc(new_power))
|
||||
var/obj/machinery/power/apc/A = new_power
|
||||
if(!is_valid_apc(new_power) || !A.terminal)
|
||||
new_power = null // don't enter an APC without a terminal or a broken APC, etc.
|
||||
|
||||
// there's no electricity in space
|
||||
if(!new_cable && !new_power && (!can_exit_cable || isspaceturf(newloc)))
|
||||
return
|
||||
|
||||
var/moved = ..()
|
||||
|
||||
if(!new_cable && !new_power)
|
||||
if(can_exit_cable && moved)
|
||||
speed = outside_cable_speed
|
||||
else
|
||||
speed = inside_cable_speed
|
||||
|
||||
if(moved)
|
||||
if(!is_under_tile() && prob(PULSEDEMON_PLATING_SPARK_CHANCE))
|
||||
do_sparks(rand(2, 4), FALSE, src)
|
||||
|
||||
current_weapon = null
|
||||
current_robot = null
|
||||
if(current_bot)
|
||||
current_bot.hijacked = FALSE
|
||||
current_bot = null
|
||||
|
||||
/*
|
||||
A few notes about this terrible proc, If you're wondering, I didn't write it but man I do NOT want to touch it
|
||||
1. A lot of this 100% shouldn't be on move, that's just waiting for something bad to happen
|
||||
2. Never, EVER directly call a do_after here, it will cause move to sleep which is awful
|
||||
*/
|
||||
if(new_power)
|
||||
current_power = new_power
|
||||
current_cable = null
|
||||
forceMove(current_power) // we go inside the machine
|
||||
RegisterSignal(current_power, COMSIG_ATOM_EMP_ACT, PROC_REF(handle_emp), TRUE)
|
||||
playsound(src, 'sound/effects/eleczap.ogg', 15, TRUE)
|
||||
do_sparks(rand(2, 4), FALSE, src)
|
||||
if(isapc(current_power))
|
||||
if(current_power in hijacked_apcs)
|
||||
update_controlling_area()
|
||||
else
|
||||
INVOKE_ASYNC(src, PROC_REF(try_hijack_apc), current_power)
|
||||
else if(new_cable)
|
||||
current_cable = new_cable
|
||||
if(current_power)
|
||||
UnregisterSignal(current_power, COMSIG_ATOM_EMP_ACT)
|
||||
current_power = null
|
||||
update_controlling_area()
|
||||
if(!isturf(loc))
|
||||
loc = get_turf(newloc)
|
||||
if(!moved)
|
||||
forceMove(newloc)
|
||||
else if(moved)
|
||||
current_cable = null
|
||||
current_power = null
|
||||
update_controlling_area()
|
||||
|
||||
// signal to replace relaymove where or when? // Never, actually just manage your code instead
|
||||
/obj/machinery/power/relaymove(mob/user, dir)
|
||||
if(!ispulsedemon(user))
|
||||
return ..()
|
||||
|
||||
var/mob/living/basic/demon/pulse_demon/demon = user
|
||||
var/turf/T = get_turf(src)
|
||||
var/turf/T2 = get_step(T, dir)
|
||||
if(demon.can_exit_cable || locate(/obj/structure/cable) in T2)
|
||||
playsound(src, 'sound/effects/eleczap.ogg', 15, TRUE)
|
||||
do_sparks(rand(2, 4), FALSE, src)
|
||||
user.forceMove(T)
|
||||
if(isapc(src))
|
||||
demon.update_controlling_area(TRUE)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/adjust_charge(amount, adjust_max = FALSE)
|
||||
if(!amount)
|
||||
return FALSE
|
||||
if(adjust_max)
|
||||
maxcharge = max(maxcharge, charge + amount)
|
||||
var/orig = charge
|
||||
charge = min(maxcharge, charge + amount)
|
||||
var/realdelta = charge - orig
|
||||
if(!realdelta)
|
||||
return FALSE
|
||||
if(realdelta > 0)
|
||||
charge_drained += realdelta
|
||||
|
||||
update_glow()
|
||||
for(var/datum/spell/pulse_demon/S in mob_spell_list)
|
||||
if(!S.action || S.locked || !S.cast_cost)
|
||||
continue
|
||||
var/dist = S.cast_cost - orig
|
||||
// only update icon if the amount is actually enough to change a spell's availability
|
||||
if(dist == 0 || (dist > 0 && realdelta >= dist) || (dist < 0 && realdelta <= dist))
|
||||
S.action.build_all_button_icons()
|
||||
return realdelta
|
||||
|
||||
// linear scale for glow strength, see table:
|
||||
// 1.5 <= 300000 ()
|
||||
// 2 at 400000
|
||||
// 2.5 at 50000
|
||||
// 3 at 600000 etc
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/update_glow()
|
||||
var/range = charge / 200000
|
||||
range = clamp(range, 1.5, 5)
|
||||
set_light(range, 2, glow_color)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/drain_APC(obj/machinery/power/apc/A, multiplier = 1)
|
||||
if(A.being_hijacked)
|
||||
return PULSEDEMON_SOURCE_DRAIN_INVALID
|
||||
// CELLRATE is the conversion ratio between a watt tick and powercell energy storage units
|
||||
var/amount_to_drain = clamp(A.cell.charge / GLOB.CELLRATE, 0, power_drain_rate * WATT_TICK_TO_JOULE * multiplier)
|
||||
A.cell.use(min(amount_to_drain * GLOB.CELLRATE, maxcharge - charge)) // calculated seperately because the apc charge multiplier shouldn't affect the actual consumption
|
||||
return adjust_charge(amount_to_drain * PULSEDEMON_APC_CHARGE_MULTIPLIER)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/drain_SMES(obj/machinery/power/smes/S, multiplier = 1)
|
||||
// CELLRATE is the conversion ratio between a watt tick and powercell energy storage units.
|
||||
var/amount_to_drain = clamp(S.charge / GLOB.CELLRATE, 0, power_drain_rate * WATT_TICK_TO_JOULE * multiplier * PULSEDEMON_SMES_DRAIN_MULTIPLIER)
|
||||
var/drained = adjust_charge(amount_to_drain)
|
||||
S.charge -= drained * GLOB.CELLRATE
|
||||
return drained
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/Life(seconds, times_fired)
|
||||
. = ..()
|
||||
|
||||
var/got_power = FALSE
|
||||
if(current_cable)
|
||||
if(current_cable.get_available_power() >= power_per_regen)
|
||||
current_cable.add_power_demand(power_per_regen)
|
||||
got_power = TRUE
|
||||
|
||||
var/excess = initial(power_per_regen) - power_per_regen
|
||||
if(excess > 0 && current_cable.get_available_power() >= excess && do_drain)
|
||||
adjust_charge(excess)
|
||||
current_cable.add_power_demand(excess)
|
||||
else if(current_power)
|
||||
if(isapc(current_power) && loc == current_power && do_drain)
|
||||
if(drain_APC(current_power) > power_per_regen)
|
||||
got_power = TRUE
|
||||
else if(istype(current_power, /obj/machinery/power/smes) && do_drain)
|
||||
if(drain_SMES(current_power) > power_per_regen)
|
||||
got_power = TRUE
|
||||
// try to take power from the powernet if the APC or SMES is empty (or we're not /really/ in the APC)
|
||||
if(!got_power && current_power.get_available_power() >= power_per_regen)
|
||||
current_power.consume_direct_power(power_per_regen)
|
||||
got_power = TRUE
|
||||
else if(!can_exit_cable)
|
||||
death()
|
||||
return
|
||||
|
||||
if(got_power)
|
||||
if(regen_lock <= 0)
|
||||
adjustHealth(-health_regen_rate)
|
||||
clear_alert(ALERT_CATEGORY_NOPOWER)
|
||||
else
|
||||
var/rate = health_loss_rate
|
||||
if(!current_cable && !current_power && can_exit_cable)
|
||||
// 2 * initial_rate - upgrade_level
|
||||
rate += initial(health_loss_rate)
|
||||
adjustHealth(rate)
|
||||
throw_alert(ALERT_CATEGORY_NOPOWER, /atom/movable/screen/alert/pulse_nopower)
|
||||
|
||||
if(regen_lock > 0)
|
||||
if(--regen_lock == 0)
|
||||
clear_alert(ALERT_CATEGORY_NOREGEN)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/gen_speech_name()
|
||||
. = ""
|
||||
for(var/i = 1 to 10)
|
||||
. += pick("!", "@", "#", "$", "%", "^", "&", "*")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/say(message, verb, sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE)
|
||||
if(client && check_mute(client.ckey, MUTE_IC))
|
||||
to_chat(src, "<span class='danger'>You cannot speak in IC (Muted).</span>")
|
||||
return FALSE
|
||||
|
||||
if(sanitize)
|
||||
message = sanitize_for_ic(trim(message))
|
||||
|
||||
if(stat)
|
||||
if(stat == DEAD)
|
||||
return say_dead(message)
|
||||
return FALSE
|
||||
|
||||
if(current_robot)
|
||||
var/turf/T = get_turf(src)
|
||||
log_say("[key_name_admin(src)] (@[T.x], [T.y], [T.z]) made [current_robot]([key_name_admin(current_robot)]) say: [message]")
|
||||
log_admin("[key_name_admin(src)] made [key_name_admin(current_robot)] say: [message]")
|
||||
message_admins("<span class='notice'>[key_name_admin(src)] made [key_name_admin(current_robot)] say: [message]</span>")
|
||||
// don't sanitize again
|
||||
current_robot.say(message, null, FALSE, ignore_speech_problems, ignore_atmospherics, ignore_languages)
|
||||
return TRUE
|
||||
|
||||
var/message_mode = parse_message_mode(message, "headset")
|
||||
|
||||
if(message_mode)
|
||||
if(message_mode == "headset")
|
||||
message = copytext(message, 2)
|
||||
else
|
||||
message = copytext(message, 3)
|
||||
|
||||
message = trim_left(message)
|
||||
|
||||
var/list/message_pieces = list()
|
||||
if(ignore_languages)
|
||||
message_pieces = message_to_multilingual(message)
|
||||
else
|
||||
message_pieces = parse_languages(message)
|
||||
|
||||
// hivemind languages
|
||||
if(istype(message_pieces, /datum/multilingual_say_piece))
|
||||
var/datum/multilingual_say_piece/S = message_pieces
|
||||
S.speaking.broadcast(src, S.message)
|
||||
return TRUE
|
||||
|
||||
if(!LAZYLEN(message_pieces))
|
||||
. = FALSE
|
||||
CRASH("Message failed to generate pieces. [message] - [json_encode(message_pieces)]")
|
||||
|
||||
create_log(SAY_LOG, "[message_mode ? "([message_mode])" : ""] '[message]'")
|
||||
|
||||
playsound(get_turf(src), pick(speech_sounds), 30, TRUE)
|
||||
if(istype(loc, /obj/item/radio))
|
||||
var/obj/item/radio/R = loc
|
||||
name = gen_speech_name()
|
||||
R.talk_into(src, message_pieces, message_mode, verbage = verb)
|
||||
name = real_name
|
||||
return TRUE
|
||||
else if(istype(loc, /obj/machinery/hologram/holopad))
|
||||
var/obj/machinery/hologram/holopad/H = loc
|
||||
name = "[H]"
|
||||
for(var/mob/M as anything in get_mobs_in_view(7, H, ai_eyes = AI_EYE_REQUIRE_HEAR))
|
||||
M.hear_say(message_pieces, verb, FALSE, src)
|
||||
name = real_name
|
||||
return TRUE
|
||||
|
||||
return TRUE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/update_runechat_msg_location()
|
||||
if(istype(loc, /obj/machinery/hologram/holopad))
|
||||
runechat_msg_location = loc.UID()
|
||||
else
|
||||
return ..()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/visible_message(message, self_message, blind_message, chat_message_type)
|
||||
// overriden because pulse demon is quite often in non-turf locs, and /mob/visible_message acts differently there
|
||||
for(var/mob/M as anything in get_mobs_in_view(7, src, ai_eyes = AI_EYE_INCLUDE))
|
||||
if(M.see_invisible < invisibility)
|
||||
continue //can't view the invisible
|
||||
var/msg = message
|
||||
if(self_message && M == src)
|
||||
msg = self_message
|
||||
M.show_message(msg, EMOTE_VISIBLE, blind_message, EMOTE_AUDIBLE, chat_message_type = MESSAGE_TYPE_LOCALCHAT)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/has_internal_radio_channel_access(mob/user, list/req_one_accesses)
|
||||
return has_access(list(), req_one_accesses, get_all_accesses())
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/try_hijack_apc(obj/machinery/power/apc/A, remote = FALSE)
|
||||
// one APC per pulse demon, one pulse demon per APC, no duplicate APCs
|
||||
if(!is_valid_apc(A) || (A in hijacked_apcs) || apc_being_hijacked || A.being_hijacked)
|
||||
return FALSE
|
||||
|
||||
to_chat(src, "<span class='notice'>You are now attempting to hijack [A], this will take approximately [hijack_time / 10] seconds.</span>")
|
||||
apc_being_hijacked = A
|
||||
A.being_hijacked = TRUE
|
||||
A.update_icon()
|
||||
if(do_after(src, hijack_time, target = A))
|
||||
if(is_valid_apc(A))
|
||||
finish_hijack_apc(A, remote)
|
||||
else
|
||||
to_chat(src, "<span class='warning'>Failed to hijack [src].</span>")
|
||||
apc_being_hijacked = null
|
||||
A.being_hijacked = FALSE
|
||||
A.update_icon()
|
||||
|
||||
// Basically this proc gives you more max charge per apc you have hijacked
|
||||
// Looks weird but it gets the job done
|
||||
/mob/living/basic/demon/pulse_demon/proc/calc_maxcharge(hijacked_apcs)
|
||||
if(!hijacked_apcs) // No APCs hijacked? No extra charge
|
||||
return 100000
|
||||
return 100000 + (hijacked_apcs * 25000)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/finish_hijack_apc(obj/machinery/power/apc/A, remote = FALSE)
|
||||
var/image/apc_image = image('icons/obj/power.dmi', A, "apcemag", ABOVE_LIGHTING_LAYER, A.dir)
|
||||
apc_image.plane = ABOVE_LIGHTING_PLANE
|
||||
LAZYADD(apc_images[get_turf(A)], apc_image)
|
||||
client.images += apc_image
|
||||
|
||||
apcs_remaining = apcs_remaining + 1
|
||||
hijacked_apcs += A
|
||||
RegisterSignal(A, COMSIG_PARENT_QDELETING, PROC_REF(apc_deleted_handler))
|
||||
if(!remote)
|
||||
update_controlling_area()
|
||||
maxcharge = calc_maxcharge(length(hijacked_apcs)) + (maxcharge - calc_maxcharge(length(hijacked_apcs) - 1))
|
||||
to_chat(src, "<span class='notice'>Hijacking complete! You now control [length(hijacked_apcs)] APCs and have [apcs_remaining] left to spend.</span>")
|
||||
var/turf/T = get_turf(A)
|
||||
var/distance = 0
|
||||
strengthen_cables(T, distance)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/strengthen_cables(turf/T, distance)
|
||||
distance += 1
|
||||
for(var/obj/structure/cable/cable in T.contents)
|
||||
if(cable.strengthened == TRUE)
|
||||
continue
|
||||
cable.strengthened = TRUE
|
||||
cable.RegisterSignal(src, COMSIG_MOB_DEATH, TYPE_PROC_REF(/obj/structure/cable, unstrengthen_cables))
|
||||
if(distance >= 15)
|
||||
return
|
||||
for(var/obj/structure/cable/cable in range(1, T))
|
||||
if(get_turf(cable) == T)
|
||||
continue
|
||||
if(cable.strengthened == TRUE)
|
||||
continue
|
||||
var/turf/next_turf = get_turf(cable)
|
||||
strengthen_cables(next_turf, distance)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/on_atom_entered(datum/source, atom/movable/entered)
|
||||
SIGNAL_HANDLER // COMSIG_ATOM_ENTERED
|
||||
try_cross_shock(entered)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/on_movable_moved(datum/source, old_location, direction, forced)
|
||||
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
|
||||
if(is_under_tile())
|
||||
return
|
||||
for(var/mob/living/mob in loc)
|
||||
try_shock_mob(mob)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/try_cross_shock(atom/movable/A)
|
||||
if(!isliving(A) || is_under_tile())
|
||||
return
|
||||
var/mob/living/L = A
|
||||
try_shock_mob(L)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/try_shock_mob(mob/living/L, siemens_coeff = 1)
|
||||
var/dealt = 0
|
||||
if(current_cable && current_cable.powernet && current_cable.powernet.available_power)
|
||||
// returns used energy, not damage dealt, but ez conversion with /20
|
||||
dealt = electrocute_mob(L, current_cable.powernet, src, siemens_coeff) / 20
|
||||
else
|
||||
dealt = L.electrocute_act(30, src, siemens_coeff)
|
||||
if(dealt > 0)
|
||||
do_sparks(rand(2, 4), FALSE, src)
|
||||
if(dealt == 0 && strong_shocks)
|
||||
if(charge >= 200 KJ)
|
||||
charge -= 200 KJ
|
||||
do_sparks(rand(2, 4), FALSE, src)
|
||||
dealt = L.electrocute_act(30, src, siemens_coeff = 1, flags = SHOCK_NOGLOVES) //bypass that nasty shock resistance
|
||||
else
|
||||
to_chat(src, "<span class = 'danger'>You dont have enough charge to bypass their insulation! You need at least 50KJ of energy!")
|
||||
|
||||
add_attack_logs(src, L, "shocked ([dealt] damage)")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/is_under_tile()
|
||||
var/turf/T = get_turf(src)
|
||||
return T.intact || HAS_TRAIT(T, TRAIT_TURF_COVERED)
|
||||
|
||||
// cable (and hijacked APC) view helper
|
||||
/mob/living/basic/demon/pulse_demon/proc/update_cableview()
|
||||
if(!client)
|
||||
return
|
||||
|
||||
// clear out old images
|
||||
for(var/image/current_image in cable_images + apc_images)
|
||||
client.images -= current_image
|
||||
|
||||
var/turf/T = get_turf(src)
|
||||
|
||||
// regenerate for all cables on our (or our holder's) z-level
|
||||
cable_images.Cut()
|
||||
for(var/datum/regional_powernet/P in SSmachines.powernets)
|
||||
for(var/obj/structure/cable/C in P.cables)
|
||||
var/turf/cable_turf = get_turf(C)
|
||||
if(T.z != cable_turf.z)
|
||||
break // skip entire powernet if it's off z-level
|
||||
|
||||
var/image/cable_image = image(C, C, layer = ABOVE_LIGHTING_LAYER, dir = C.dir)
|
||||
// good visibility here
|
||||
cable_image.plane = ABOVE_LIGHTING_PLANE
|
||||
LAZYADD(cable_images[cable_turf], cable_image)
|
||||
client.images += cable_image
|
||||
|
||||
// same for hijacked APCs
|
||||
apc_images.Cut()
|
||||
for(var/obj/machinery/power/apc/A in hijacked_apcs)
|
||||
var/turf/apc_turf = get_turf(A)
|
||||
if(T.z != apc_turf.z)
|
||||
continue
|
||||
// parent of image is the APC, not the turf because of how clicking on images works
|
||||
var/image/apc_image = image('icons/obj/power.dmi', A, "apcemag", ABOVE_LIGHTING_LAYER, A.dir)
|
||||
apc_image.plane = ABOVE_LIGHTING_PLANE
|
||||
LAZYADD(apc_images[apc_turf], apc_image)
|
||||
client.images += apc_image
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/handle_emp(datum/source, severity)
|
||||
SIGNAL_HANDLER
|
||||
if(emp_debounce)
|
||||
return
|
||||
visible_message("<span class='danger'>[src] [pick("fizzles", "wails", "flails")] in anguish!</span>")
|
||||
playsound(get_turf(src), pick(hurt_sounds), 30, TRUE)
|
||||
throw_alert(ALERT_CATEGORY_NOREGEN, /atom/movable/screen/alert/pulse_noregen)
|
||||
switch(severity)
|
||||
if(EMP_LIGHT)
|
||||
adjustHealth(round(max(initial(health) / 4, round(maxHealth / 8))))
|
||||
regen_lock = 5
|
||||
if(EMP_HEAVY)
|
||||
adjustHealth(round(max(initial(health) / 3, round(maxHealth / 6))))
|
||||
regen_lock = 8
|
||||
emp_debounce = TRUE
|
||||
addtimer(VARSET_CALLBACK(src, emp_debounce, FALSE), 0.1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/try_attack_mob(mob/living/L)
|
||||
if(!is_under_tile() && L != src)
|
||||
do_attack_animation(L)
|
||||
try_shock_mob(L)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/UnarmedAttack(atom/A)
|
||||
if(isliving(A))
|
||||
try_attack_mob(A)
|
||||
else if(isitem(A) && !is_under_tile())
|
||||
var/obj/item/O = A
|
||||
var/obj/item/stock_parts/cell/C = O.get_cell()
|
||||
if(C?.charge)
|
||||
C.use(min(C.charge, power_drain_rate))
|
||||
adjust_charge(min(C.charge, power_drain_rate))
|
||||
visible_message("<span class='notice'>[src] touches [O] and drains its power!</span>", "<span class='notice'>You touch [O] and drain it's power!</span>")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/attack_hand(mob/living/carbon/human/M)
|
||||
if(is_under_tile())
|
||||
to_chat(M, "<span class='danger'>You can't interact with something that's under the floor!</span>")
|
||||
return
|
||||
switch(M.intent)
|
||||
if(INTENT_HELP)
|
||||
visible_message("<span class='notice'>[M] [response_help_continuous] [src].</span>")
|
||||
if(INTENT_DISARM, INTENT_GRAB)
|
||||
visible_message("<span class='notice'>[M] [response_disarm_continuous] [src].</span>")
|
||||
if(INTENT_HELP)
|
||||
visible_message("<span class='warning'>[M] [response_harm_continuous] [src].</span>")
|
||||
try_attack_mob(M)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/attack_by(obj/item/O, mob/living/user, params)
|
||||
if(..())
|
||||
return FINISH_ATTACK
|
||||
|
||||
if(is_under_tile())
|
||||
to_chat(user, "<span class='danger'>You can't interact with something that's under the floor!</span>")
|
||||
return FINISH_ATTACK
|
||||
|
||||
var/obj/item/stock_parts/cell/C = O.get_cell()
|
||||
if(C && C.charge)
|
||||
C.use(min(C.charge, power_drain_rate))
|
||||
adjust_charge(min(C.charge, power_drain_rate))
|
||||
to_chat(user, "<span class='warning'>You touch [src] with [O] and [src] drains it!</span>")
|
||||
to_chat(src, "<span class='notice'>[user] touches you with [O] and you drain its power!</span>")
|
||||
visible_message("<span class='notice'>[O] goes right through [src].</span>")
|
||||
try_shock_mob(user, O.siemens_coefficient)
|
||||
return FINISH_ATTACK
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/ex_act()
|
||||
return
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/CanPass(atom/movable/mover, border_dir)
|
||||
. = ..()
|
||||
if(istype(mover, /obj/projectile/ion))
|
||||
return FALSE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/bullet_act(obj/projectile/proj)
|
||||
if(proj.damage_type == BURN)
|
||||
regen_lock = max(regen_lock, 1)
|
||||
return ..()
|
||||
else
|
||||
visible_message("<span class='warning'>[proj] goes right through [src]!</span>")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/electrocute_act(shock_damage, source, siemens_coeff, flags)
|
||||
return
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/blob_act(obj/structure/blob/B)
|
||||
return // will likely end up dying if the blob cuts its wires anyway
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/narsie_act()
|
||||
return // you can't turn electricity into a harvester
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/get_access()
|
||||
return get_all_accesses()
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/IsAdvancedToolUser()
|
||||
return TRUE // interacting with machines
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/can_be_pulled()
|
||||
return FALSE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/can_buckle()
|
||||
return FALSE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
|
||||
return
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/experience_pressure_difference(flow_x, flow_y)
|
||||
return // Immune to gas flow.
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/singularity_pull()
|
||||
return
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/mob_negates_gravity()
|
||||
return TRUE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/mob_has_gravity()
|
||||
return TRUE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/can_remote_apc_interface(obj/machinery/power/apc/ourapc)
|
||||
if(ourapc.hacked_by_ruin_AI || ourapc.malfai || ourapc.malfhack)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/adjustHealth(amount, updating_health)
|
||||
if(amount > 0) // This damages the pulse demon
|
||||
return ..()
|
||||
|
||||
if(!ismachinery(loc))
|
||||
if(health >= (maxHealth / 2))
|
||||
amount = 0
|
||||
else
|
||||
amount = clamp(amount, -((maxHealth / 2) - health), 0)
|
||||
amount = round(amount, 1)
|
||||
return ..()
|
||||
|
||||
/atom/movable/screen/alert/pulse_nopower
|
||||
name = "No Power"
|
||||
desc = "You are not connected to a cable or machine and are losing health!"
|
||||
icon_state = "pd_nopower"
|
||||
|
||||
/atom/movable/screen/alert/pulse_noregen
|
||||
name = "Regeneration Stalled"
|
||||
desc = "You've been EMP'd and cannot regenerate health!"
|
||||
icon_state = "pd_noregen"
|
||||
|
||||
#undef ALERT_CATEGORY_NOPOWER
|
||||
#undef ALERT_CATEGORY_NOREGEN
|
||||
|
||||
#undef PULSEDEMON_PLATING_SPARK_CHANCE
|
||||
#undef PULSEDEMON_APC_CHARGE_MULTIPLIER
|
||||
#undef PULSEDEMON_SMES_DRAIN_MULTIPLIER
|
||||
@@ -0,0 +1,255 @@
|
||||
/mob/living/basic/demon/pulse_demon/ClickOn(atom/A, params)
|
||||
if(client?.click_intercept)
|
||||
client.click_intercept.InterceptClickOn(src, params, A)
|
||||
return
|
||||
|
||||
if(next_click > world.time)
|
||||
return
|
||||
|
||||
if(try_modified_click(A, params))
|
||||
return
|
||||
|
||||
// escape out of whatever we've hijacked first
|
||||
if(isapc(A) && !isturf(loc) && (A in hijacked_apcs))
|
||||
A.attack_pulsedemon(src)
|
||||
else if(current_weapon && istype(current_weapon, /obj/item/gun/energy))
|
||||
// ironically, because ion guns override their emp_act, it's perfectly safe to be in one and be emp'd
|
||||
var/obj/item/gun/energy/G = current_weapon
|
||||
// we probably shouldn't be firing from inside a recharger or someone's bag
|
||||
if(iscarbon(G.loc) || isturf(G.loc))
|
||||
G.process_fire(A, src, FALSE)
|
||||
visible_message("<span class='danger'>[G] fires itself at [A]!</span>", "<span class='danger'>You force [G] to fire at [A]!</span>", "<span class='italics'>You hear \a [G.fire_sound_text]!</span>")
|
||||
changeNext_click(CLICK_CD_RANGE) // I can't actually find what the default gun fire cooldown is, so it's 1 second until someone enlightens me
|
||||
return
|
||||
else if(current_robot)
|
||||
log_admin("[key_name_admin(src)] made [key_name_admin(current_robot)] attack [A]")
|
||||
message_admins("<span class='notice'>[key_name_admin(src)] made [key_name_admin(current_robot)] attack [A]</span>")
|
||||
|
||||
current_robot.ClickOn(A, params)
|
||||
changeNext_click(0.5 SECONDS)
|
||||
return
|
||||
else if(current_bot)
|
||||
if(A == current_bot)
|
||||
A.attack_ai(src)
|
||||
else
|
||||
current_bot.attack_integrated_pulsedemon(src, A)
|
||||
changeNext_click(0.5 SECONDS)
|
||||
return
|
||||
else if(get_area(A) == controlling_area)
|
||||
A.attack_pulsedemon(src)
|
||||
else
|
||||
..()
|
||||
changeNext_click(0.1 SECONDS)
|
||||
|
||||
// returns TRUE if any [modifier]ClickOn was called
|
||||
/mob/living/basic/demon/pulse_demon/proc/try_modified_click(atom/A, params)
|
||||
var/list/modifiers = params2list(params)
|
||||
if(modifiers["middle"])
|
||||
if(modifiers["shift"])
|
||||
MiddleShiftClickOn(A)
|
||||
else
|
||||
MiddleClickOn(A)
|
||||
return TRUE
|
||||
if(modifiers["shift"])
|
||||
ShiftClickOn(A)
|
||||
return TRUE
|
||||
if(modifiers["alt"])
|
||||
AltClickOn(A)
|
||||
return TRUE
|
||||
if(modifiers["ctrl"])
|
||||
CtrlClickOn(A)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
// check area for all of these, then do AI actions
|
||||
/mob/living/basic/demon/pulse_demon/MiddleShiftClickOn(atom/A)
|
||||
if(get_area(A) == controlling_area)
|
||||
A.AIShiftMiddleClick(src)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/ShiftClickOn(atom/A)
|
||||
if(get_area(A) == controlling_area)
|
||||
A.AIShiftClick(src)
|
||||
else
|
||||
examinate(A)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/AltClickOn(atom/A)
|
||||
if(get_area(A) == controlling_area)
|
||||
A.AIAltClick(src)
|
||||
else
|
||||
AltClickNoInteract(src, A)
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/CtrlClickOn(atom/A)
|
||||
if(get_area(A) == controlling_area)
|
||||
A.AICtrlClick(src)
|
||||
|
||||
// for alt-click status tab
|
||||
/mob/living/basic/demon/pulse_demon/TurfAdjacent(turf/T)
|
||||
return get_area(T) == controlling_area || ..()
|
||||
|
||||
// for overrides in general
|
||||
/atom/proc/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
return
|
||||
|
||||
/obj/machinery/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
return attack_ai(user)
|
||||
|
||||
// ai not allowed to use cams consoles
|
||||
/obj/machinery/computer/security/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
return attack_hand(user)
|
||||
|
||||
// jump back into our apc
|
||||
/obj/machinery/power/apc/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
if(user.loc != src)
|
||||
user.forceMove(src)
|
||||
user.current_power = src
|
||||
user.update_controlling_area()
|
||||
else
|
||||
attack_ai(user)
|
||||
|
||||
/mob/living/simple_animal/bot/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
if(user.loc == src)
|
||||
return
|
||||
to_chat(user, "<span class='warning'>You are now inside [src]. If it is destroyed, you will be dropped onto the ground, and may die if there is no cable under you.</span>")
|
||||
to_chat(user, "<span class='notice'>Leave it by jumping to a hijacked APC.</span>")
|
||||
ejectpai(user)
|
||||
user.forceMove(src)
|
||||
user.current_bot = src
|
||||
hijacked = TRUE
|
||||
|
||||
/mob/living/simple_animal/bot/relaymove(mob/user, dir)
|
||||
if(!on)
|
||||
to_chat(user, "[src] isn't turned on!")
|
||||
return
|
||||
if(ispulsedemon(user))
|
||||
var/mob/living/basic/demon/pulse_demon/demon = user
|
||||
if(demon.bot_movedelay <= world.time && dir)
|
||||
Move(get_step(get_turf(src), dir))
|
||||
demon.bot_movedelay = world.time + (BOT_STEP_DELAY * (base_speed - 1)) * ((dir in GLOB.diagonals) ? SQRT_2 : 1)
|
||||
|
||||
/obj/machinery/recharger/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
user.forceMove(src)
|
||||
if(!charging)
|
||||
to_chat(user, "<span class='warning'>There is no weapon charging. Click again to retry.</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You are now attempting to hijack [src], this will take approximately [user.hijack_time / 10] seconds.</span>")
|
||||
if(!do_after(user, user.hijack_time, FALSE, src))
|
||||
return
|
||||
if(!charging)
|
||||
to_chat(src, "<span class='warning'>Failed to hijack [src]</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You are now inside [charging]. Click on a hijacked APC to return.</span>")
|
||||
user.forceMove(charging)
|
||||
user.current_weapon = charging
|
||||
|
||||
/obj/machinery/cell_charger/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
user.forceMove(src)
|
||||
if(!charging)
|
||||
to_chat(user, "<span class='warning'>There is no cell charging. Click again to retry.</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You are now attempting to hijack [src], this will take approximately [user.hijack_time / 10] seconds.</span>")
|
||||
if(charging.rigged)
|
||||
to_chat(user, "<span class='notice'>You are now inside [charging]. Click on a hijacked APC to return.</span>")
|
||||
user.forceMove(charging)
|
||||
return
|
||||
if(!do_after(user, user.hijack_time, FALSE, src))
|
||||
return
|
||||
if(!charging)
|
||||
to_chat(src, "<span class='warning'>Failed to hijack [src].</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You are now inside [charging]. Click on a hijacked APC to return.</span>")
|
||||
user.forceMove(charging)
|
||||
|
||||
/obj/machinery/recharge_station/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
user.forceMove(src)
|
||||
if(!isrobot(occupant))
|
||||
to_chat(user, "<span class='warning'>There is no silicon-based occupant inside. Click again to retry.</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You are now attempting to hijack [occupant], this will take approximately [user.hijack_time / 10] seconds.</span>")
|
||||
var/mob/living/silicon/robot/R = occupant
|
||||
if(R in user.hijacked_robots)
|
||||
user.do_hijack_robot(occupant)
|
||||
return
|
||||
to_chat(R, "<span class='userdanger'>ALERT: ELECTRICAL MALEVOLENCE DETECTED, TARGETING SYSTEMS HIJACK IN PROGRESS</span>")
|
||||
if(!do_after(user, user.hijack_time, FALSE, src))
|
||||
return
|
||||
if(isrobot(occupant))
|
||||
user.do_hijack_robot(occupant)
|
||||
return
|
||||
to_chat(src, "<span class='warning'>Failed to hijack [src].</span>")
|
||||
|
||||
/mob/living/basic/demon/pulse_demon/proc/do_hijack_robot(mob/living/silicon/robot/R)
|
||||
to_chat(src, "<span class='notice'>You are now inside [R]. Click on a hijacked APC to return.</span>")
|
||||
forceMove(R)
|
||||
current_robot = R
|
||||
if(!(R in hijacked_robots))
|
||||
hijacked_robots += R
|
||||
to_chat(R, "<span class='userdanger'>TARGETING SYSTEMS HIJACKED, REPORT ALL UNWANTED ACTIVITY</span>")
|
||||
|
||||
/obj/machinery/camera/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
if(user.loc != src)
|
||||
user.forceMove(src)
|
||||
to_chat(user, "<span class='notice'>You jump towards [src]. Click on a hijacked APC to return.</span>")
|
||||
|
||||
// see pulse_demon/say
|
||||
/obj/machinery/hologram/holopad/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
if(user.loc != src)
|
||||
user.forceMove(src)
|
||||
to_chat(user, "<span class='notice'>You jump towards [src]. You can now communicate via the holopad's speaker. Click on a hijacked APC to return.</span>")
|
||||
|
||||
/obj/item/radio/attack_pulsedemon(mob/living/basic/demon/pulse_demon/user)
|
||||
if(user.loc != src)
|
||||
user.forceMove(src)
|
||||
to_chat(user, "<span class='notice'>You jump towards [src]. You can now communicate via radio. Click on a hijacked APC to return.</span>")
|
||||
else
|
||||
attack_ai(user)
|
||||
|
||||
/mob/living/simple_animal/bot/proc/attack_integrated_pulsedemon(mob/living/basic/demon/pulse_demon/user, atom/A)
|
||||
if(!on)
|
||||
return
|
||||
if(Adjacent(A))
|
||||
UnarmedAttack(A)
|
||||
else
|
||||
RangedAttack(A)
|
||||
|
||||
/mob/living/simple_animal/bot/secbot/attack_integrated_pulsedemon(mob/living/basic/demon/pulse_demon/user, atom/A)
|
||||
if(!on)
|
||||
return
|
||||
if(Adjacent(A))
|
||||
UnarmedAttack(A)
|
||||
else if(iscarbon(A))
|
||||
speak("Level 10 infraction alert!")
|
||||
playsound(loc, pick('sound/voice/bcriminal.ogg', 'sound/voice/bjustice.ogg', 'sound/voice/bfreeze.ogg'), 50)
|
||||
visible_message("<b>[src]</b> points at [A.name]!")
|
||||
|
||||
/mob/living/simple_animal/bot/floorbot/attack_integrated_pulsedemon(mob/living/basic/demon/pulse_demon/user, atom/A)
|
||||
if(!on)
|
||||
return
|
||||
if(isfloorturf(A) && Adjacent(A))
|
||||
var/turf/simulated/floor/F = A
|
||||
// there was originally a 1% chance to break to lattice, but that doesn't help a pulse demon, so I don't see the point
|
||||
F.break_tile_to_plating()
|
||||
audible_message("<span class='danger'>[src] makes an excited booping sound.</span>")
|
||||
|
||||
/mob/living/simple_animal/bot/cleanbot/attack_integrated_pulsedemon(mob/living/basic/demon/pulse_demon/user, atom/A)
|
||||
if(!on)
|
||||
return
|
||||
if(isfloorturf(A) && Adjacent(A))
|
||||
var/turf/simulated/floor/F = A
|
||||
if(prob(50))
|
||||
F.MakeSlippery(TURF_WET_WATER)
|
||||
if(prob(50))
|
||||
visible_message("<span class='warning'>Something flies out of [src]! It seems to be acting oddly.</span>")
|
||||
if(!(locate(/obj/effect/decal/cleanable/blood/gibs) in F))
|
||||
new /obj/effect/decal/cleanable/blood/gibs(F)
|
||||
playsound(F, 'sound/effects/blobattack.ogg', 40, TRUE)
|
||||
|
||||
/mob/living/simple_animal/bot/mulebot/attack_integrated_pulsedemon(mob/living/basic/demon/pulse_demon/user, atom/A)
|
||||
if(!on)
|
||||
return
|
||||
if(istype(A) && Adjacent(A) && ismovable(A))
|
||||
to_chat(user, "<span class='notice'You try to load [A] onto [src].</span>")
|
||||
load(A)
|
||||
return
|
||||
if(load)
|
||||
to_chat(user, "<span class='notice'You unload [load].</span>")
|
||||
unload(0)
|
||||
@@ -0,0 +1,197 @@
|
||||
/mob/living/basic/demon/shadow
|
||||
name = "shadow demon"
|
||||
desc = "A creature that's barely tangible, you can feel its gaze piercing you."
|
||||
icon = 'icons/mob/mob.dmi'
|
||||
icon_state = "shadow_demon"
|
||||
icon_living = "shadow_demon"
|
||||
move_resist = MOVE_FORCE_STRONG
|
||||
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE // so they can tell where the darkness is
|
||||
see_in_dark = 10 // Below 10 `see_in_dark`, you'll not have full vision with fullscreen
|
||||
loot = list(/obj/item/organ/internal/heart/demon/shadow)
|
||||
death_sound = 'sound/shadowdemon/shadowdeath.ogg'
|
||||
unsuitable_atmos_damage = 18 // You will heal very slowly in a vacuum (2 damage). Go back to the air to heal faster.
|
||||
var/thrown_alert = FALSE
|
||||
var/wrapping = FALSE
|
||||
/// Should only be TRUE if we're shooting the Shadow Grapple right now. If its TRUE, the demon wont be able to shadow crawl.
|
||||
var/block_shadow_crawl = FALSE
|
||||
/// Used for fault tolerance. If you set it manually, it may fuck up shooting_grapple var.
|
||||
var/last_block_shadow_crawl = 0
|
||||
|
||||
/mob/living/basic/demon/shadow/Login()
|
||||
..()
|
||||
var/list/L = list(
|
||||
"<span class='deadsay'><font size=3><b>You are a shadow demon!</b></font></span>",
|
||||
"<b>You are a lethal ambush predator who thrives in the darkness, calling upon the shadows to heal your injured form and increase your speed.</b>",
|
||||
"<b>Light is however your worst enemy and being exposed for too long will be fatal.</b>",
|
||||
"<b>Striking your victims with your shadow grapple extinguishes any light sources around them. Striking items silences any light within them.</b>",
|
||||
"<b>You can wrap your dead victims into a shadow cocoon which provides a shroud of darkness which tears away any light near it.</b>",
|
||||
"<b><i>You do not remember anything of your past lives, nor will you remember anything about this one after your death.</i></b>",
|
||||
"<br><span class='motd'>For more information, check the wiki page: [wiki_link("Shadow_Demon")]</span>"
|
||||
)
|
||||
to_chat(src, chat_box_red(L.Join("<br>")))
|
||||
|
||||
/mob/living/basic/demon/shadow/Life(seconds, times_fired)
|
||||
. = ..()
|
||||
var/lum_count = check_darkness()
|
||||
var/damage_mod = istype(loc, /obj/effect/dummy/slaughter) ? 0.5 : 1
|
||||
if(lum_count > 0.2)
|
||||
adjustBruteLoss(40 * damage_mod) // 10 seconds in light
|
||||
SEND_SOUND(src, sound('sound/weapons/sear.ogg'))
|
||||
to_chat(src, "<span class='biggerdanger'>The light scalds you!</span>")
|
||||
else
|
||||
adjustBruteLoss(-20)
|
||||
|
||||
/mob/living/basic/demon/shadow/UnarmedAttack(atom/A)
|
||||
// Pick a random attack sound for each attack
|
||||
attack_sound = pick('sound/shadowdemon/shadowattack2.ogg', 'sound/shadowdemon/shadowattack3.ogg', 'sound/shadowdemon/shadowattack4.ogg')
|
||||
if(!ishuman(A))
|
||||
if(isitem(A))
|
||||
A.extinguish_light()
|
||||
return ..()
|
||||
var/mob/living/carbon/human/target = A
|
||||
if(target.stat != DEAD)
|
||||
return ..()
|
||||
|
||||
if(isLivingSSD(target) && client.send_ssd_warning(target)) // Similar to revenants, only wrap SSD targets if you've accepted the SSD warning
|
||||
return
|
||||
|
||||
if(wrapping)
|
||||
to_chat(src, "<span class='notice'>We are already wrapping something.</span>")
|
||||
return
|
||||
|
||||
visible_message("<span class='danger'>[src] begins wrapping [target] in shadowy threads.</span>")
|
||||
wrapping = TRUE
|
||||
if(!do_after(src, 4 SECONDS, FALSE, target = target))
|
||||
wrapping = FALSE
|
||||
return
|
||||
|
||||
target.visible_message("<span class='warning'><b>[src] envelops [target] into an ethereal cocoon, and darkness begins to creep from it.</b></span>")
|
||||
var/obj/structure/shadowcocoon/C = new(get_turf(target))
|
||||
target.extinguish_light() // may as well be safe
|
||||
target.forceMove(C)
|
||||
wrapping = FALSE
|
||||
|
||||
/mob/living/basic/demon/shadow/proc/block_shadow_crawl()
|
||||
last_block_shadow_crawl = world.time
|
||||
block_shadow_crawl = TRUE
|
||||
addtimer(CALLBACK(src, PROC_REF(check_block_shadow_crawl), last_block_shadow_crawl), 10 SECONDS)
|
||||
|
||||
/mob/living/basic/demon/shadow/proc/unblock_shadow_crawl()
|
||||
last_block_shadow_crawl = 0
|
||||
block_shadow_crawl = FALSE
|
||||
|
||||
/mob/living/basic/demon/shadow/proc/check_block_shadow_crawl(block_time)
|
||||
if(block_time == last_block_shadow_crawl)
|
||||
/// it means 10 seconds passed from last shadow grapple shot and it didnt unset block_shadow_crawl
|
||||
to_chat(src, "<span class='warning'>You feel good enough to use Shadow Crawl again.</span>")
|
||||
unblock_shadow_crawl()
|
||||
|
||||
/obj/structure/shadowcocoon
|
||||
name = "shadowy cocoon"
|
||||
desc = "Something wrapped in what seems to be manifested darkness. Its surface distorts unnaturally, and it emanates deep shadows."
|
||||
icon = 'icons/effects/effects.dmi'
|
||||
icon_state = "shadowcocoon"
|
||||
light_power = -4
|
||||
light_range = 6
|
||||
max_integrity = 100
|
||||
light_color = "#ddd6cf"
|
||||
anchored = TRUE
|
||||
/// Amount of SSobj ticks (Roughly 2 seconds) since the last hallucination proc'd
|
||||
var/time_since_last_hallucination = 0
|
||||
/// Will we play hallucination sounds or not
|
||||
var/silent = TRUE
|
||||
|
||||
/obj/structure/shadowcocoon/Initialize(mapload)
|
||||
. = ..()
|
||||
playsound(loc, 'sound/shadowdemon/shadownode.ogg', 5, TRUE, -1)
|
||||
START_PROCESSING(SSobj, src)
|
||||
|
||||
|
||||
/obj/structure/shadowcocoon/examine(mob/user)
|
||||
. = ..()
|
||||
if(istype(user, /mob/living/basic/demon/shadow))
|
||||
. += silent ? "<span class='notice'>The tendrils are idle and will not produce noise.</span>" : "<span class='notice'>The tendrils are agitated <b>and will occasionally produce noise to lure in more prey.</b></span>"
|
||||
. += "<span class='notice'>Alt+Click to toggle whether [src] should produce noise to lure in victims.</span>"
|
||||
|
||||
/obj/structure/shadowcocoon/process()
|
||||
time_since_last_hallucination++
|
||||
for(var/atom/to_darken in range(4, src))
|
||||
if(prob(60) || !length(to_darken.light_sources))
|
||||
continue
|
||||
if(iswelder(to_darken) && length(to_darken.light_sources))
|
||||
var/obj/item/weldingtool/welder_to_darken = to_darken
|
||||
welder_to_darken.remove_fuel(welder_to_darken.reagents.get_reagent_amount("fuel"))
|
||||
welder_to_darken.visible_message("<span class='notice'>The shadows swarm around and overwhelm the flame of [welder_to_darken].</span>")
|
||||
return
|
||||
if(istype(to_darken, /obj/item/flashlight/flare))
|
||||
var/obj/item/flashlight/flare/flare_to_darken = to_darken
|
||||
if(!flare_to_darken.on)
|
||||
continue
|
||||
flare_to_darken.turn_off()
|
||||
flare_to_darken.fuel = 0
|
||||
flare_to_darken.visible_message("<span class='notice'>[flare_to_darken] suddenly dims.</span>")
|
||||
to_darken.extinguish_light()
|
||||
if(!silent && time_since_last_hallucination >= rand(8, 12))
|
||||
playsound(src, pick('sound/shadowdemon/shadowhalluc1.ogg', 'sound/shadowdemon/shadowhalluc2.ogg', 'sound/machines/airlock_open.ogg', 'sound/machines/airlock_close.ogg', 'sound/machines/boltsup.ogg', 'sound/shadowdemon/shadowhalluc3.ogg', get_sfx("bodyfall"), 'sound/weapons/egloves.ogg'), 50)
|
||||
time_since_last_hallucination = 0
|
||||
|
||||
|
||||
// Allows you to turn on cocoons making hallucination sounds or not
|
||||
/obj/structure/shadowcocoon/AltClick(mob/user)
|
||||
if(!isdemon(user))
|
||||
return ..()
|
||||
if(silent)
|
||||
to_chat(user, "<span class='notice'>You twist and change your trapped victim in [src] to lure in more prey.</span>")
|
||||
silent = FALSE
|
||||
return
|
||||
to_chat(user, "<span class='notice'>The tendrils from [src] snap back to their orignal form.</span>")
|
||||
silent = TRUE
|
||||
|
||||
/obj/structure/shadowcocoon/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
|
||||
if(damage_type != BURN) //I unashamedly stole this from spider cocoon code
|
||||
return
|
||||
playsound(loc, 'sound/items/welder.ogg', 100, TRUE)
|
||||
|
||||
/obj/structure/shadowcocoon/obj_destruction()
|
||||
visible_message("<span class='danger'>[src] splits open, and the shadows dancing around it fade.</span>")
|
||||
return ..()
|
||||
|
||||
/obj/structure/shadowcocoon/Destroy()
|
||||
for(var/atom/movable/A in contents)
|
||||
A.forceMove(loc)
|
||||
return..()
|
||||
|
||||
/mob/living/basic/demon/shadow/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
|
||||
if(isliving(AM)) // when a living creature is thrown at it, dont knock it back
|
||||
return
|
||||
..()
|
||||
|
||||
/mob/living/basic/demon/shadow/Initialize(mapload)
|
||||
. = ..()
|
||||
AddSpell(new /datum/spell/fireball/shadow_grapple)
|
||||
var/datum/spell/bloodcrawl/shadow_crawl/S = new
|
||||
AddSpell(S)
|
||||
whisper_action.button_icon_state = "shadow_whisper"
|
||||
whisper_action.background_icon_state = "shadow_demon_bg"
|
||||
if(istype(loc, /obj/effect/dummy/slaughter))
|
||||
S.phased = TRUE
|
||||
RegisterSignal(loc, COMSIG_MOVABLE_MOVED, TYPE_PROC_REF(/mob/living/basic/demon/shadow, check_darkness))
|
||||
RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(check_darkness))
|
||||
add_overlay(emissive_appearance(icon, "shadow_demon_eye_glow_overlay"))
|
||||
|
||||
/mob/living/basic/demon/shadow/proc/check_darkness()
|
||||
var/turf/T = get_turf(src)
|
||||
var/lum_count = T.get_lumcount()
|
||||
if(lum_count > 0.2)
|
||||
if(!thrown_alert)
|
||||
thrown_alert = TRUE
|
||||
throw_alert("light", /atom/movable/screen/alert/lightexposure)
|
||||
alpha = 255
|
||||
speed = initial(speed)
|
||||
else
|
||||
if(thrown_alert)
|
||||
thrown_alert = FALSE
|
||||
clear_alert("light")
|
||||
alpha = 125
|
||||
speed = 0.5
|
||||
return lum_count
|
||||
@@ -0,0 +1,168 @@
|
||||
/// The Monster
|
||||
|
||||
/mob/living/basic/demon/slaughter
|
||||
name = "slaughter demon"
|
||||
real_name = "slaughter demon"
|
||||
desc = "A large, menacing creature covered in armored black scales. You should run."
|
||||
maxHealth = 240
|
||||
health = 240
|
||||
icon = 'icons/mob/mob.dmi'
|
||||
icon_state = "daemon"
|
||||
icon_living = "daemon"
|
||||
var/boost = 0
|
||||
var/feast_sound = 'sound/misc/demon_consume.ogg'
|
||||
var/devoured = 0
|
||||
var/list/consumed_mobs = list()
|
||||
|
||||
var/list/nearby_mortals = list()
|
||||
var/cooldown = 0
|
||||
var/gorecooldown = 0
|
||||
var/vialspawned = FALSE
|
||||
loot = list(/obj/effect/decal/cleanable/blood/innards, /obj/effect/decal/cleanable/blood, /obj/effect/gibspawner/generic, /obj/effect/gibspawner/generic, /obj/item/organ/internal/heart/demon/slaughter)
|
||||
var/playstyle_string = "<B>You are the Slaughter Demon, a terrible creature from another existence. You have a single desire: to kill. \
|
||||
You may use the blood crawl icon when on blood pools to travel through them, appearing and dissapearing from the station at will. \
|
||||
Pulling a dead or critical mob while you enter a pool will pull them in with you, allowing you to feast. \
|
||||
You move quickly upon leaving a pool of blood, but the material world will soon sap your strength and leave you sluggish. </B>"
|
||||
death_message = "screams in anger as it collapses into a puddle of viscera!"
|
||||
|
||||
/mob/living/basic/demon/slaughter/New()
|
||||
..()
|
||||
remove_from_all_data_huds()
|
||||
ADD_TRAIT(src, TRAIT_BLOODCRAWL_EAT, "bloodcrawl_eat")
|
||||
var/datum/spell/bloodcrawl/bloodspell = new
|
||||
AddSpell(bloodspell)
|
||||
if(istype(loc, /obj/effect/dummy/slaughter))
|
||||
bloodspell.phased = TRUE
|
||||
addtimer(CALLBACK(src, PROC_REF(attempt_objectives)), 5 SECONDS)
|
||||
|
||||
/mob/living/basic/demon/slaughter/Life(seconds, times_fired)
|
||||
..()
|
||||
if(boost < world.time)
|
||||
speed = 1
|
||||
else
|
||||
speed = 0
|
||||
|
||||
/mob/living/basic/demon/slaughter/proc/attempt_objectives()
|
||||
if(mind)
|
||||
var/list/messages = list()
|
||||
messages.Add(playstyle_string)
|
||||
messages.Add("<b><span class ='notice'>You are not currently in the same plane of existence as the station. Use the blood crawl action at a blood pool to manifest.</span></b>")
|
||||
SEND_SOUND(src, sound('sound/misc/demon_dies.ogg'))
|
||||
if(!vialspawned)
|
||||
SSticker.mode.traitors |= mind
|
||||
mind.add_mind_objective(/datum/objective/slaughter)
|
||||
mind.add_mind_objective(/datum/objective/demon_fluff)
|
||||
messages.Add(mind.prepare_announce_objectives(FALSE))
|
||||
|
||||
messages.Add("<span class='motd'>For more information, check the wiki page: ([GLOB.configuration.url.wiki_url]/index.php/Slaughter_Demon)</span>")
|
||||
to_chat(src, chat_box_red(messages.Join("<br>")))
|
||||
|
||||
/obj/effect/decal/cleanable/blood/innards
|
||||
icon = 'icons/obj/surgery.dmi'
|
||||
icon_state = "innards"
|
||||
base_icon = 'icons/obj/surgery.dmi'
|
||||
icon_state = "innards"
|
||||
random_icon_states = list("innards")
|
||||
name = "pile of viscera"
|
||||
desc = "A repulsive pile of guts and gore."
|
||||
weightless_icon = 'icons/obj/surgery.dmi'
|
||||
|
||||
/mob/living/basic/demon/slaughter/Destroy()
|
||||
// Only execute the below if we successfully died
|
||||
for(var/mob/living/M in consumed_mobs)
|
||||
REMOVE_TRAIT(M, TRAIT_UNREVIVABLE, "demon")
|
||||
release_consumed(M)
|
||||
. = ..()
|
||||
|
||||
/mob/living/basic/demon/slaughter/proc/release_consumed(mob/living/M)
|
||||
M.forceMove(get_turf(src))
|
||||
|
||||
// Midround slaughter demon, less tanky
|
||||
/mob/living/basic/demon/slaughter/lesser
|
||||
maxHealth = 170
|
||||
health = 170
|
||||
|
||||
// Cult slaughter demon
|
||||
/// Summoned as part of the cult objective "Bring the Slaughter"
|
||||
/mob/living/basic/demon/slaughter/cult
|
||||
name = "harbinger of the slaughter"
|
||||
real_name = "harbinger of the Slaughter"
|
||||
desc = "An awful creature from beyond the realms of madness."
|
||||
maxHealth = 540
|
||||
health = 540
|
||||
melee_damage_upper = 60
|
||||
melee_damage_lower = 60
|
||||
environment_smash = ENVIRONMENT_SMASH_RWALLS // Smashes through EVERYTHING - r-walls included
|
||||
faction = list("cult")
|
||||
playstyle_string = "<b><span class='userdanger'>You are a Harbinger of the Slaughter.</span> Brought forth by the servants of Nar'Sie, you have a single purpose: slaughter the heretics \
|
||||
who do not worship your master. You may use the ability 'Blood Crawl' near a pool of blood to enter it and become incorporeal. Using the ability again near a blood pool will allow you \
|
||||
to emerge from it. You are fast, powerful, and almost invincible. By dragging a dead or unconscious body into a blood pool with you, you will consume it after a time and fully regain \
|
||||
your health. You may use the ability 'Sense Victims' in your Cultist tab to locate a random, living heretic.</span></b>"
|
||||
|
||||
/mob/living/basic/demon/slaughter/cult/New()
|
||||
..()
|
||||
spawn(5)
|
||||
var/list/demon_candidates = SSghost_spawns.poll_candidates("Do you want to play as a slaughter demon?", ROLE_DEMON, TRUE, 10 SECONDS, source = /mob/living/basic/demon/slaughter/cult)
|
||||
if(!length(demon_candidates))
|
||||
visible_message("<span class='warning'>[src] disappears in a flash of red light!</span>")
|
||||
qdel(src)
|
||||
return
|
||||
if(QDELETED(src)) // Just in case
|
||||
return
|
||||
var/mob/M = pick(demon_candidates)
|
||||
var/mob/living/basic/demon/slaughter/cult/S = src
|
||||
if(!M || !M.client)
|
||||
visible_message("<span class='warning'>[src] disappears in a flash of red light!</span>")
|
||||
qdel(src)
|
||||
return
|
||||
var/client/C = M.client
|
||||
|
||||
S.key = C.key
|
||||
dust_if_respawnable(M)
|
||||
S.mind.assigned_role = "Harbinger of the Slaughter"
|
||||
S.mind.special_role = "Harbinger of the Slaughter"
|
||||
to_chat(S, playstyle_string)
|
||||
S.mind.add_antag_datum(/datum/antagonist/cultist)
|
||||
var/datum/spell/sense_victims/SV = new
|
||||
AddSpell(SV)
|
||||
|
||||
S.mind.add_mind_objective(/datum/objective/cult_slaughter)
|
||||
var/list/messages = S.mind.prepare_announce_objectives(FALSE)
|
||||
to_chat(S, chat_box_red(messages.Join("<br>")))
|
||||
|
||||
/mob/living/basic/demon/slaughter/laughter
|
||||
// The laughter demon! It's everyone's best friend! It just wants to hug
|
||||
// them so much, it wants to hug everyone at once!
|
||||
name = "laughter demon"
|
||||
real_name = "laughter demon"
|
||||
desc = "A large, adorable creature covered in armor with pink bows."
|
||||
speak_emote = list("giggles", "titters", "chuckles", "gaffaws", "laughs")
|
||||
response_help_continuous = "hugs"
|
||||
response_help_simple = "hug"
|
||||
attack_verb_continuous = "wildly tickles"
|
||||
attack_verb_simple = "wildly tickles"
|
||||
maxHealth = 215
|
||||
health = 215
|
||||
melee_damage_lower = 25
|
||||
melee_damage_upper = 25
|
||||
playstyle_string = "<B>You are the Laughter Demon, an adorable creature from another existence. You have a single desire: to hug and tickle. \
|
||||
You may use the blood crawl icon when on blood pools to travel through them, appearing and dissapearing from the station at will. \
|
||||
Pulling a dead or critical mob while you enter a pool will pull them in with you, allowing you to hug them. \
|
||||
You move quickly upon leaving a pool of blood, but the material world will soon sap your strength and leave you sluggish. \
|
||||
(You should be attacking people on harm intent, and not nuzzling them.)</B>"
|
||||
|
||||
attack_sound = 'sound/items/bikehorn.ogg'
|
||||
feast_sound = 'sound/spookoween/scary_horn2.ogg'
|
||||
death_sound = 'sound/misc/sadtrombone.ogg'
|
||||
|
||||
icon_state = "bowmon"
|
||||
icon_living = "bowmon"
|
||||
death_message = "fades out, as all of its friends are released from its prison of hugs."
|
||||
loot = list(/mob/living/simple_animal/pet/cat/kitten{name = "Laughter"})
|
||||
|
||||
/mob/living/basic/demon/slaughter/laughter/release_consumed(mob/living/M)
|
||||
if(M.revive())
|
||||
M.grab_ghost(force = TRUE)
|
||||
playsound(get_turf(src), feast_sound, 50, TRUE, -1)
|
||||
to_chat(M, "<span class='clown'>You leave [src]'s warm embrace, and feel ready to take on the world.</span>")
|
||||
..(M)
|
||||
Reference in New Issue
Block a user