mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-07-16 01:23:41 +01:00
8aeceae7dc
* Fixes basic mobs being lit ablaze invisibly * Snowflakes
910 lines
33 KiB
Plaintext
910 lines
33 KiB
Plaintext
// 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, TRAIT_NOFIRE)
|
|
|
|
/// 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_WARNING("<font size=3><b>You are a pulse demon.</b></font>"))
|
|
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_MOTD("For more information, check the wiki page: ([GLOB.configuration.url.wiki_url]/index.php/Pulse_Demon)"))
|
|
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_DANGER("Your self-sustaining ability has automatically enabled itself to prevent death from having no connection!"))
|
|
|
|
/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_DANGER("You cannot speak in IC (Muted)."))
|
|
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_NOTICE("[key_name_admin(src)] made [key_name_admin(current_robot)] say: [message]"))
|
|
// 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_NOTICE("You are now attempting to hijack [A], this will take approximately [hijack_time / 10] seconds."))
|
|
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_WARNING("Failed to hijack [src]."))
|
|
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_NOTICE("Hijacking complete! You now control [length(hijacked_apcs)] APCs and have [apcs_remaining] left to spend."))
|
|
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_DANGER("[src] [pick("fizzles", "wails", "flails")] in anguish!"))
|
|
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_NOTICE("[src] touches [O] and drains its power!"), SPAN_NOTICE("You touch [O] and drain it's power!"))
|
|
|
|
/mob/living/basic/demon/pulse_demon/attack_hand(mob/living/carbon/human/M)
|
|
if(is_under_tile())
|
|
to_chat(M, SPAN_DANGER("You can't interact with something that's under the floor!"))
|
|
return
|
|
switch(M.intent)
|
|
if(INTENT_HELP)
|
|
visible_message(SPAN_NOTICE("[M] [response_help_continuous] [src]."))
|
|
if(INTENT_DISARM, INTENT_GRAB)
|
|
visible_message(SPAN_NOTICE("[M] [response_disarm_continuous] [src]."))
|
|
if(INTENT_HELP)
|
|
visible_message(SPAN_WARNING("[M] [response_harm_continuous] [src]."))
|
|
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_DANGER("You can't interact with something that's under the floor!"))
|
|
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_WARNING("You touch [src] with [O] and [src] drains it!"))
|
|
to_chat(src, SPAN_NOTICE("[user] touches you with [O] and you drain its power!"))
|
|
visible_message(SPAN_NOTICE("[O] goes right through [src]."))
|
|
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_WARNING("[proj] goes right through [src]!"))
|
|
|
|
/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
|