mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 01:34:01 +00:00
## About The Pull Request This PR tackles our piss-poor item action handling. Currently in order to make an item only have actions when its equipped to a certain slot you need to override a proc, which I've changed by introducing an action_slots variable. I've also cleaned up a ton of action code, and most importantly moved a lot of Trigger effects on items to do_effect, which allows actions to not call ui_action_click or attack_self on an item without bypassing IsAvailible and comsigs that parent Trigger has. This resolves issues like jump boots being usable from your hands, HUDs being toggleable out of your pockets, etc. Also moved a few actions from relying on attack_self to individual handling on their side. This also stops welding masks/hardhats from showing their action while you hold them, this part of the change is just something I thought didn't make much sense - you can use their action by using them in-hand, and flickering on your action bar can be annoying when reshuffling your backpack. Closes #89653 ## Why It's Good For The Game Makes action handling significantly less ass, allows us to avoid code like this ```js /obj/item/clothing/mask/gas/sechailer/ui_action_click(mob/user, action) if(istype(action, /datum/action/item_action/halt)) halt() else adjust_visor(user) ```
1297 lines
51 KiB
Plaintext
1297 lines
51 KiB
Plaintext
//This file contains all Lavaland megafauna loot. Does not include crusher trophies.
|
|
|
|
|
|
//Hierophant: Hierophant Club
|
|
|
|
#define HIEROPHANT_BLINK_RANGE 5
|
|
#define HIEROPHANT_BLINK_COOLDOWN (15 SECONDS)
|
|
|
|
/datum/action/innate/dash/hierophant
|
|
current_charges = 1
|
|
max_charges = 1
|
|
charge_rate = HIEROPHANT_BLINK_COOLDOWN
|
|
recharge_sound = null
|
|
phasein = /obj/effect/temp_visual/hierophant/blast/visual
|
|
phaseout = /obj/effect/temp_visual/hierophant/blast/visual
|
|
// It's a simple purple beam, works well enough for the purple hiero effects.
|
|
beam_effect = "plasmabeam"
|
|
|
|
/datum/action/innate/dash/hierophant/teleport(mob/user, atom/target)
|
|
var/dist = get_dist(user, target)
|
|
if(dist > HIEROPHANT_BLINK_RANGE)
|
|
user.balloon_alert(user, "destination out of range!")
|
|
return FALSE
|
|
var/turf/target_turf = get_turf(target)
|
|
if(target_turf.is_blocked_turf_ignore_climbable())
|
|
user.balloon_alert(user, "destination blocked!")
|
|
return FALSE
|
|
|
|
. = ..()
|
|
var/obj/item/hierophant_club/club = target
|
|
if(!istype(club))
|
|
return
|
|
|
|
club.update_appearance(UPDATE_ICON_STATE)
|
|
|
|
/datum/action/innate/dash/hierophant/charge()
|
|
. = ..()
|
|
var/obj/item/hierophant_club/club = target
|
|
if(!istype(club))
|
|
return
|
|
|
|
club.update_appearance(UPDATE_ICON_STATE)
|
|
|
|
/obj/item/hierophant_club
|
|
name = "hierophant club"
|
|
desc = "The strange technology of this large club allows various nigh-magical teleportation feats. It used to beat you, but now you can set the beat."
|
|
icon_state = "hierophant_club_ready_beacon"
|
|
inhand_icon_state = "hierophant_club_ready_beacon"
|
|
icon_angle = -135
|
|
icon = 'icons/obj/mining_zones/artefacts.dmi'
|
|
lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/64x64_righthand.dmi'
|
|
inhand_x_dimension = 64
|
|
inhand_y_dimension = 64
|
|
slot_flags = ITEM_SLOT_BACK
|
|
w_class = WEIGHT_CLASS_NORMAL
|
|
force = 15
|
|
attack_verb_continuous = list("clubs", "beats", "pummels")
|
|
attack_verb_simple = list("club", "beat", "pummel")
|
|
hitsound = 'sound/items/weapons/sonic_jackhammer.ogg'
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
|
actions_types = list(/datum/action/item_action/vortex_recall)
|
|
action_slots = ALL
|
|
/// Linked teleport beacon for the group teleport functionality.
|
|
var/obj/effect/hierophant/beacon
|
|
/// TRUE if currently doing a teleport to the beacon, FALSE otherwise.
|
|
var/teleporting = FALSE //if we ARE teleporting
|
|
/// Action enabling the blink-dash functionality.
|
|
var/datum/action/innate/dash/hierophant/blink
|
|
/// Whether the blink ability is activated. IF TRUE, left clicking a location will blink to it. If FALSE, this is disabled.
|
|
var/blink_activated = TRUE
|
|
|
|
/obj/item/hierophant_club/Initialize(mapload)
|
|
. = ..()
|
|
AddElement(/datum/element/update_icon_updates_onmob)
|
|
blink = new(src)
|
|
|
|
/obj/item/hierophant_club/Destroy()
|
|
QDEL_NULL(blink)
|
|
return ..()
|
|
|
|
/obj/item/hierophant_club/examine(mob/user)
|
|
. = ..()
|
|
. += span_hierophant_warning("The[beacon ? " beacon is not currently":"re is a beacon"] attached.")
|
|
|
|
/obj/item/hierophant_club/suicide_act(mob/living/user)
|
|
say("Xverwpsgexmrk...", forced = "hierophant club suicide")
|
|
user.visible_message(span_suicide("[user] holds [src] into the air! It looks like [user.p_theyre()] trying to commit suicide!"))
|
|
new/obj/effect/temp_visual/hierophant/telegraph(get_turf(user))
|
|
playsound(user,'sound/machines/airlock/airlockopen.ogg', 75, TRUE)
|
|
user.visible_message(span_hierophant_warning("[user] fades out, leaving [user.p_their()] belongings behind!"))
|
|
for(var/obj/item/user_item in user)
|
|
if(user_item != src)
|
|
user.dropItemToGround(user_item)
|
|
for(var/turf/blast_turf in RANGE_TURFS(1, user))
|
|
new /obj/effect/temp_visual/hierophant/blast/visual(blast_turf, user, TRUE)
|
|
user.dropItemToGround(src) //Drop us last, so it goes on top of their stuff
|
|
qdel(user)
|
|
|
|
/obj/item/hierophant_club/attack_self(mob/user)
|
|
blink_activated = !blink_activated
|
|
to_chat(user, span_notice("You [blink_activated ? "enable" : "disable"] the blink function on [src]."))
|
|
|
|
/obj/item/hierophant_club/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
// If our target is the beacon and the hierostaff is next to the beacon, we're trying to pick it up.
|
|
if(interacting_with == beacon)
|
|
return NONE
|
|
if(blink_activated)
|
|
blink.teleport(user, interacting_with)
|
|
return ITEM_INTERACT_SUCCESS
|
|
return NONE
|
|
|
|
/obj/item/hierophant_club/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
if(blink_activated)
|
|
blink.teleport(user, interacting_with)
|
|
return ITEM_INTERACT_SUCCESS
|
|
return NONE
|
|
|
|
/obj/item/hierophant_club/update_icon_state()
|
|
icon_state = inhand_icon_state = "hierophant_club[blink?.current_charges > 0 ? "_ready":""][(!QDELETED(beacon)) ? "":"_beacon"]"
|
|
return ..()
|
|
|
|
/obj/item/hierophant_club/ui_action_click(mob/user, action)
|
|
if(!user.is_holding(src)) //you need to hold the staff to teleport
|
|
to_chat(user, span_warning("You need to hold the club in your hands to [beacon ? "teleport with it":"detach the beacon"]!"))
|
|
return
|
|
if(!beacon || QDELETED(beacon))
|
|
if(isturf(user.loc))
|
|
user.visible_message(span_hierophant_warning("[user] starts fiddling with [src]'s pommel..."), \
|
|
span_notice("You start detaching the hierophant beacon..."))
|
|
if(do_after(user, 5 SECONDS, target = user) && !beacon)
|
|
var/turf/user_turf = get_turf(user)
|
|
playsound(user_turf,'sound/effects/magic/blind.ogg', 200, TRUE, -4)
|
|
new /obj/effect/temp_visual/hierophant/telegraph/teleport(user_turf, user)
|
|
beacon = new/obj/effect/hierophant(user_turf)
|
|
user.update_mob_action_buttons()
|
|
user.visible_message(span_hierophant_warning("[user] places a strange machine beneath [user.p_their()] feet!"), \
|
|
"[span_hierophant("You detach the hierophant beacon, allowing you to teleport yourself and any allies to it at any time!")]\n\
|
|
[span_notice("You can remove the beacon to place it again by striking it with the club.")]")
|
|
update_appearance(UPDATE_ICON_STATE)
|
|
else
|
|
to_chat(user, span_warning("You need to be on solid ground to detach the beacon!"))
|
|
return
|
|
if(get_dist(user, beacon) <= 2) //beacon too close abort
|
|
to_chat(user, span_warning("You are too close to the beacon to teleport to it!"))
|
|
return
|
|
var/turf/beacon_turf = get_turf(beacon)
|
|
if(beacon_turf?.is_blocked_turf(TRUE))
|
|
to_chat(user, span_warning("The beacon is blocked by something, preventing teleportation!"))
|
|
return
|
|
if(!isturf(user.loc))
|
|
to_chat(user, span_warning("You don't have enough space to teleport from here!"))
|
|
return
|
|
teleporting = TRUE //start channel
|
|
user.update_mob_action_buttons()
|
|
user.visible_message(span_hierophant_warning("[user] starts to glow faintly..."))
|
|
beacon.icon_state = "hierophant_tele_on"
|
|
var/obj/effect/temp_visual/hierophant/telegraph/edge/user_telegraph = new /obj/effect/temp_visual/hierophant/telegraph/edge(user.loc)
|
|
var/obj/effect/temp_visual/hierophant/telegraph/edge/beacon_telegraph = new /obj/effect/temp_visual/hierophant/telegraph/edge(beacon.loc)
|
|
if(do_after(user, 4 SECONDS, target = user) && user && beacon)
|
|
var/turf/destination = get_turf(beacon)
|
|
var/turf/source = get_turf(user)
|
|
if(destination.is_blocked_turf(TRUE))
|
|
teleporting = FALSE
|
|
to_chat(user, span_warning("The beacon is blocked by something, preventing teleportation!"))
|
|
user.update_mob_action_buttons()
|
|
beacon.icon_state = "hierophant_tele_off"
|
|
return
|
|
new /obj/effect/temp_visual/hierophant/telegraph(destination, user)
|
|
new /obj/effect/temp_visual/hierophant/telegraph(source, user)
|
|
playsound(destination,'sound/effects/magic/wand_teleport.ogg', 200, TRUE)
|
|
playsound(source,'sound/machines/airlock/airlockopen.ogg', 200, TRUE)
|
|
if(!do_after(user, 0.3 SECONDS, target = user) || !user || !beacon || QDELETED(beacon)) //no walking away shitlord
|
|
teleporting = FALSE
|
|
if(user)
|
|
user.update_mob_action_buttons()
|
|
if(beacon)
|
|
beacon.icon_state = "hierophant_tele_off"
|
|
return
|
|
if(destination.is_blocked_turf(TRUE))
|
|
teleporting = FALSE
|
|
to_chat(user, span_warning("The beacon is blocked by something, preventing teleportation!"))
|
|
user.update_mob_action_buttons()
|
|
beacon.icon_state = "hierophant_tele_off"
|
|
return
|
|
user.log_message("teleported self from [AREACOORD(source)] to [beacon].", LOG_GAME)
|
|
new /obj/effect/temp_visual/hierophant/telegraph/teleport(destination, user)
|
|
new /obj/effect/temp_visual/hierophant/telegraph/teleport(source, user)
|
|
for(var/turf_near_beacon in RANGE_TURFS(1, destination))
|
|
new /obj/effect/temp_visual/hierophant/blast/visual(turf_near_beacon, user, TRUE)
|
|
for(var/turf_near_source in RANGE_TURFS(1, source))
|
|
new /obj/effect/temp_visual/hierophant/blast/visual(turf_near_source, user, TRUE)
|
|
for(var/mob/living/mob_to_teleport in range(1, source))
|
|
INVOKE_ASYNC(src, PROC_REF(teleport_mob), source, mob_to_teleport, destination, user)
|
|
sleep(0.6 SECONDS) //at this point the blasts detonate
|
|
if(beacon)
|
|
beacon.icon_state = "hierophant_tele_off"
|
|
else
|
|
qdel(user_telegraph)
|
|
qdel(beacon_telegraph)
|
|
if(beacon)
|
|
beacon.icon_state = "hierophant_tele_off"
|
|
teleporting = FALSE
|
|
if(user)
|
|
user.update_mob_action_buttons()
|
|
|
|
/obj/item/hierophant_club/proc/teleport_mob(turf/source, mob/teleporting, turf/target, mob/user)
|
|
var/turf/turf_to_teleport_to = get_step(target, get_dir(source, teleporting)) //get position relative to caster
|
|
if(!turf_to_teleport_to || turf_to_teleport_to.is_blocked_turf(TRUE))
|
|
return
|
|
animate(teleporting, alpha = 0, time = 2, easing = EASE_OUT) //fade out
|
|
sleep(0.1 SECONDS)
|
|
if(!teleporting)
|
|
return
|
|
teleporting.visible_message(span_hierophant_warning("[teleporting] fades out!"))
|
|
sleep(0.2 SECONDS)
|
|
if(!teleporting)
|
|
return
|
|
var/success = do_teleport(teleporting, turf_to_teleport_to, no_effects = TRUE, channel = TELEPORT_CHANNEL_MAGIC)
|
|
sleep(0.1 SECONDS)
|
|
if(!teleporting)
|
|
return
|
|
animate(teleporting, alpha = 255, time = 2, easing = EASE_IN) //fade IN
|
|
sleep(0.1 SECONDS)
|
|
if(!teleporting)
|
|
return
|
|
teleporting.visible_message(span_hierophant_warning("[teleporting] fades in!"))
|
|
if(user != teleporting && success)
|
|
log_combat(user, teleporting, "teleported", null, "from [AREACOORD(source)]")
|
|
|
|
/obj/item/hierophant_club/equipped(mob/user)
|
|
. = ..()
|
|
blink.Grant(user, src)
|
|
user.update_icons()
|
|
|
|
/obj/item/hierophant_club/dropped(mob/user)
|
|
. = ..()
|
|
blink.Remove(user)
|
|
user.update_icons()
|
|
|
|
#undef HIEROPHANT_BLINK_RANGE
|
|
#undef HIEROPHANT_BLINK_COOLDOWN
|
|
|
|
//Bubblegum: Mayhem in a Bottle, H.E.C.K. Suit, Soulscythe
|
|
|
|
/obj/item/mayhem
|
|
name = "mayhem in a bottle"
|
|
desc = "A magically infused bottle of blood, the scent of which will drive anyone nearby into a murderous frenzy."
|
|
icon = 'icons/obj/mining_zones/artefacts.dmi'
|
|
icon_state = "vial"
|
|
|
|
/obj/item/mayhem/attack_self(mob/user)
|
|
if(tgui_alert(user, "Breaking the bottle will cause nearby crewmembers to go into a murderous frenzy. Be sure you know what you are doing...","Break the bottle?",list("Break it!","DON'T")) != "Break it!")
|
|
return
|
|
if(QDELETED(src) || !user.is_holding(src) || user.incapacitated)
|
|
return
|
|
for(var/mob/living/carbon/human/target in range(7,user))
|
|
target.apply_status_effect(/datum/status_effect/mayhem)
|
|
to_chat(user, span_notice("You shatter the bottle!"))
|
|
playsound(user.loc, 'sound/effects/glass/glassbr1.ogg', 100, TRUE)
|
|
message_admins(span_adminnotice("[ADMIN_LOOKUPFLW(user)] has activated a bottle of mayhem!"))
|
|
user.log_message("activated a bottle of mayhem", LOG_ATTACK)
|
|
qdel(src)
|
|
|
|
/obj/item/clothing/suit/hooded/hostile_environment
|
|
name = "H.E.C.K. suit"
|
|
desc = "Hostile Environment Cross-Kinetic Suit: A suit designed to withstand the wide variety of hazards from Lavaland. It wasn't enough for its last owner."
|
|
icon_state = "hostile_env"
|
|
hoodtype = /obj/item/clothing/head/hooded/hostile_environment
|
|
armor_type = /datum/armor/hooded_hostile_environment
|
|
cold_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
|
|
min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT
|
|
heat_protection = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
|
|
max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
|
|
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
|
|
clothing_flags = THICKMATERIAL|HEADINTERNALS
|
|
resistance_flags = FIRE_PROOF|LAVA_PROOF|ACID_PROOF
|
|
transparent_protection = HIDESUITSTORAGE|HIDEJUMPSUIT
|
|
allowed = null
|
|
greyscale_colors = "#4d4d4d#808080"
|
|
greyscale_config = /datum/greyscale_config/heck_suit
|
|
greyscale_config_worn = /datum/greyscale_config/heck_suit/worn
|
|
greyscale_config_worn_digi = /datum/greyscale_config/heck_suit/worn/digi //SKYRAT EDIT ADDITION - DigiGreyscale
|
|
flags_1 = IS_PLAYER_COLORABLE_1
|
|
|
|
/datum/armor/hooded_hostile_environment
|
|
melee = 70
|
|
bullet = 40
|
|
laser = 10
|
|
energy = 20
|
|
bomb = 50
|
|
fire = 100
|
|
acid = 100
|
|
|
|
/obj/item/clothing/suit/hooded/hostile_environment/Initialize(mapload)
|
|
. = ..()
|
|
AddElement(/datum/element/radiation_protected_clothing)
|
|
AddElement(/datum/element/gags_recolorable)
|
|
allowed = GLOB.mining_suit_allowed
|
|
|
|
/obj/item/clothing/suit/hooded/hostile_environment/process(seconds_per_tick)
|
|
var/mob/living/carbon/wearer = loc
|
|
if(istype(wearer) && SPT_PROB(1, seconds_per_tick)) //cursed by bubblegum
|
|
if(prob(7.5))
|
|
wearer.cause_hallucination(/datum/hallucination/oh_yeah, "H.E.C.K suit", haunt_them = TRUE)
|
|
else
|
|
if(HAS_TRAIT(wearer, TRAIT_ANOSMIA)) //Anosmia quirk holder cannot fell any smell
|
|
to_chat(wearer, span_warning("[pick("You hear faint whispers.","You feel hot.","You hear a roar in the distance.")]"))
|
|
else
|
|
to_chat(wearer, span_warning("[pick("You hear faint whispers.","You smell ash.","You feel hot.","You hear a roar in the distance.")]"))
|
|
|
|
/obj/item/clothing/head/hooded/hostile_environment
|
|
name = "H.E.C.K. helmet"
|
|
icon = 'icons/obj/clothing/head/helmet.dmi'
|
|
worn_icon = 'icons/mob/clothing/head/helmet.dmi'
|
|
desc = "Hostile Environment Cross-Kinetic Helmet: A helmet designed to withstand the wide variety of hazards from Lavaland. It wasn't enough for its last owner."
|
|
icon_state = "hostile_env"
|
|
w_class = WEIGHT_CLASS_NORMAL
|
|
armor_type = /datum/armor/hooded_hostile_environment
|
|
cold_protection = HEAD
|
|
min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT
|
|
heat_protection = HEAD
|
|
max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
|
|
clothing_flags = SNUG_FIT|THICKMATERIAL
|
|
resistance_flags = FIRE_PROOF|LAVA_PROOF|ACID_PROOF
|
|
flags_inv = HIDEMASK|HIDEEARS|HIDEFACE|HIDEEYES|HIDEHAIR|HIDEFACIALHAIR|HIDESNOUT
|
|
flags_cover = HEADCOVERSMOUTH
|
|
actions_types = list()
|
|
greyscale_colors = "#4d4d4d#808080#ff3300"
|
|
greyscale_config = /datum/greyscale_config/heck_helmet
|
|
greyscale_config_worn = /datum/greyscale_config/heck_helmet/worn
|
|
greyscale_config_worn_digi = /datum/greyscale_config/heck_helmet/worn/snouted //SKYRAT EDIT ADDITION - MuzzledGreyscale (Why does this use worn_digi)
|
|
flags_1 = IS_PLAYER_COLORABLE_1
|
|
|
|
/obj/item/clothing/head/hooded/hostile_environment/Initialize(mapload)
|
|
. = ..()
|
|
update_appearance()
|
|
AddComponent(/datum/component/butchering/wearable, \
|
|
speed = 0.5 SECONDS, \
|
|
effectiveness = 150, \
|
|
bonus_modifier = 0, \
|
|
butcher_sound = null, \
|
|
disabled = null, \
|
|
can_be_blunt = TRUE, \
|
|
butcher_callback = CALLBACK(src, PROC_REF(consume)), \
|
|
)
|
|
AddElement(/datum/element/radiation_protected_clothing)
|
|
AddElement(/datum/element/gags_recolorable)
|
|
|
|
/obj/item/clothing/head/hooded/hostile_environment/equipped(mob/user, slot, initial = FALSE)
|
|
. = ..()
|
|
to_chat(user, span_notice("You feel a bloodlust. You can now butcher corpses with your bare arms."))
|
|
|
|
/obj/item/clothing/head/hooded/hostile_environment/dropped(mob/user, silent = FALSE)
|
|
. = ..()
|
|
to_chat(user, span_notice("You lose your bloodlust."))
|
|
|
|
/obj/item/clothing/head/hooded/hostile_environment/proc/consume(mob/living/user, mob/living/butchered)
|
|
if(butchered.mob_biotypes & (MOB_ROBOTIC | MOB_SPIRIT))
|
|
return
|
|
var/health_consumed = butchered.maxHealth * 0.1
|
|
user.heal_ordered_damage(health_consumed, list(BRUTE, BURN, TOX))
|
|
to_chat(user, span_notice("You heal from the corpse of [butchered]."))
|
|
var/datum/client_colour/color = user.add_client_colour(/datum/client_colour/bloodlust)
|
|
QDEL_IN(color, 1 SECONDS)
|
|
|
|
#define MAX_BLOOD_LEVEL 100
|
|
|
|
/obj/item/soulscythe
|
|
name = "soulscythe"
|
|
desc = "An old relic of hell created by devils to establish themselves as the leadership of hell over the demons. It grows stronger while it possesses a powerful soul."
|
|
icon = 'icons/obj/mining_zones/artefacts.dmi'
|
|
icon_state = "soulscythe"
|
|
inhand_icon_state = "soulscythe"
|
|
lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/64x64_righthand.dmi'
|
|
attack_verb_continuous = list("chops", "slices", "cuts", "reaps")
|
|
attack_verb_simple = list("chop", "slice", "cut", "reap")
|
|
hitsound = 'sound/items/weapons/bladeslice.ogg'
|
|
inhand_x_dimension = 64
|
|
inhand_y_dimension = 64
|
|
force = 20
|
|
throwforce = 17
|
|
armour_penetration = 50
|
|
sharpness = SHARP_EDGED
|
|
bare_wound_bonus = 10
|
|
layer = MOB_LAYER
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
|
/// Soulscythe mob in the scythe
|
|
var/mob/living/simple_animal/soulscythe/soul
|
|
/// Are we grabbing a spirit?
|
|
var/using = FALSE
|
|
/// Currently charging?
|
|
var/charging = FALSE
|
|
/// Cooldown between moves
|
|
COOLDOWN_DECLARE(move_cooldown)
|
|
/// Cooldown between attacks
|
|
COOLDOWN_DECLARE(attack_cooldown)
|
|
|
|
/obj/item/soulscythe/Initialize(mapload)
|
|
. = ..()
|
|
soul = new(src)
|
|
RegisterSignal(soul, COMSIG_LIVING_RESIST, PROC_REF(on_resist))
|
|
RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED, PROC_REF(on_attack))
|
|
RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED_SECONDARY, PROC_REF(on_secondary_attack))
|
|
RegisterSignal(src, COMSIG_ATOM_INTEGRITY_CHANGED, PROC_REF(on_integrity_change))
|
|
AddElement(/datum/element/bane, mob_biotypes = MOB_PLANT, damage_multiplier = 0.5, requires_combat_mode = FALSE)
|
|
|
|
/obj/item/soulscythe/examine(mob/user)
|
|
. = ..()
|
|
. += soul.ckey ? span_nicegreen("There is a soul inhabiting it.") : span_danger("It's dormant.")
|
|
|
|
/obj/item/soulscythe/attack(mob/living/attacked, mob/living/user, params)
|
|
. = ..()
|
|
if(attacked.stat != DEAD)
|
|
give_blood(10)
|
|
|
|
/obj/item/soulscythe/attack_hand(mob/user, list/modifiers)
|
|
if(soul.ckey && !soul.faction_check_atom(user))
|
|
to_chat(user, span_warning("You can't pick up [src]!"))
|
|
return
|
|
return ..()
|
|
|
|
/obj/item/soulscythe/pickup(mob/user)
|
|
. = ..()
|
|
if(soul.ckey)
|
|
animate(src) //stop spinnage
|
|
|
|
/obj/item/soulscythe/dropped(mob/user, silent)
|
|
. = ..()
|
|
if(soul.ckey)
|
|
reset_spin() //resume spinnage
|
|
|
|
/obj/item/soulscythe/attack_self(mob/user, modifiers)
|
|
if(using || soul.ckey || soul.stat)
|
|
return
|
|
if(!(GLOB.ghost_role_flags & GHOSTROLE_STATION_SENTIENCE))
|
|
balloon_alert(user, "you can't awaken the scythe!")
|
|
return
|
|
using = TRUE
|
|
balloon_alert(user, "you hold the scythe up...")
|
|
ADD_TRAIT(src, TRAIT_NODROP, type)
|
|
var/mob/chosen_one = SSpolling.poll_ghosts_for_target(
|
|
check_jobban = ROLE_PAI,
|
|
poll_time = 20 SECONDS,
|
|
checked_target = src,
|
|
ignore_category = POLL_IGNORE_POSSESSED_BLADE,
|
|
alert_pic = src,
|
|
role_name_text = "soulscythe soul",
|
|
chat_text_border_icon = src,
|
|
)
|
|
on_poll_concluded(user, chosen_one)
|
|
|
|
/// Ghost poll has concluded and a candidate has been chosen.
|
|
/obj/item/soulscythe/proc/on_poll_concluded(mob/living/master, mob/dead/observer/ghost)
|
|
if(isnull(ghost))
|
|
balloon_alert(master, "the scythe is dormant!")
|
|
REMOVE_TRAIT(src, TRAIT_NODROP, type)
|
|
using = FALSE
|
|
return
|
|
|
|
soul.PossessByPlayer(ghost.ckey)
|
|
soul.copy_languages(master, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the master.
|
|
soul.faction = list("[REF(master)]")
|
|
balloon_alert(master, "the scythe glows")
|
|
add_overlay("soulscythe_gem")
|
|
density = TRUE
|
|
if(!ismob(loc))
|
|
reset_spin()
|
|
|
|
REMOVE_TRAIT(src, TRAIT_NODROP, type)
|
|
using = FALSE
|
|
|
|
/obj/item/soulscythe/relaymove(mob/living/user, direction)
|
|
if(!COOLDOWN_FINISHED(src, move_cooldown) || charging)
|
|
return
|
|
if(!isturf(loc))
|
|
balloon_alert(user, "resist out!")
|
|
COOLDOWN_START(src, move_cooldown, 1 SECONDS)
|
|
return
|
|
if(!use_blood(1, FALSE))
|
|
return
|
|
if(pixel_x != base_pixel_x || pixel_y != base_pixel_y)
|
|
animate(src, 0.2 SECONDS, pixel_x = base_pixel_y, pixel_y = base_pixel_y, flags = ANIMATION_PARALLEL)
|
|
try_step_multiz(direction)
|
|
COOLDOWN_START(src, move_cooldown, (direction in GLOB.cardinals) ? 0.1 SECONDS : 0.2 SECONDS)
|
|
|
|
/obj/item/soulscythe/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
|
|
. = ..()
|
|
if(!charging)
|
|
return
|
|
charging = FALSE
|
|
throwforce *= 0.5
|
|
reset_spin()
|
|
if(ismineralturf(hit_atom))
|
|
var/turf/closed/mineral/hit_rock = hit_atom
|
|
hit_rock.gets_drilled()
|
|
if(isliving(hit_atom))
|
|
var/mob/living/hit_mob = hit_atom
|
|
if(hit_mob.stat != DEAD)
|
|
give_blood(15)
|
|
|
|
/obj/item/soulscythe/AllowClick()
|
|
return TRUE
|
|
|
|
/obj/item/soulscythe/proc/use_blood(amount = 0, message = TRUE)
|
|
if(amount > soul.blood_level)
|
|
if(message)
|
|
to_chat(soul, span_warning("Not enough blood!"))
|
|
return FALSE
|
|
soul.blood_level -= amount
|
|
return TRUE
|
|
|
|
/obj/item/soulscythe/proc/give_blood(amount)
|
|
soul.blood_level = min(MAX_BLOOD_LEVEL, soul.blood_level + amount)
|
|
|
|
/obj/item/soulscythe/proc/on_resist(mob/living/user)
|
|
SIGNAL_HANDLER
|
|
|
|
if(isturf(loc))
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(break_out))
|
|
|
|
/obj/item/soulscythe/proc/break_out()
|
|
if(!use_blood(10))
|
|
return
|
|
balloon_alert(soul, "you resist...")
|
|
if(!do_after(soul, 5 SECONDS, target = src, timed_action_flags = IGNORE_TARGET_LOC_CHANGE))
|
|
balloon_alert(soul, "interrupted!")
|
|
return
|
|
balloon_alert(soul, "you break out")
|
|
if(ismob(loc))
|
|
var/mob/holder = loc
|
|
holder.temporarilyRemoveItemFromInventory(src)
|
|
forceMove(drop_location())
|
|
|
|
/obj/item/soulscythe/proc/on_integrity_change(datum/source, old_value, new_value)
|
|
SIGNAL_HANDLER
|
|
|
|
soul.set_health(new_value)
|
|
|
|
/obj/item/soulscythe/proc/on_attack(mob/living/source, atom/attacked_atom, modifiers)
|
|
SIGNAL_HANDLER
|
|
|
|
if(!COOLDOWN_FINISHED(src, attack_cooldown) || !isturf(loc))
|
|
return
|
|
if(get_dist(source, attacked_atom) > 1)
|
|
INVOKE_ASYNC(src, PROC_REF(shoot_target), attacked_atom)
|
|
else
|
|
INVOKE_ASYNC(src, PROC_REF(slash_target), attacked_atom)
|
|
|
|
/obj/item/soulscythe/proc/on_secondary_attack(mob/living/source, atom/attacked_atom, modifiers)
|
|
SIGNAL_HANDLER
|
|
|
|
if(!COOLDOWN_FINISHED(src, attack_cooldown) || !isturf(loc))
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(charge_target), attacked_atom)
|
|
|
|
/obj/item/soulscythe/proc/shoot_target(atom/attacked_atom)
|
|
if(!use_blood(15))
|
|
return
|
|
COOLDOWN_START(src, attack_cooldown, 3 SECONDS)
|
|
var/obj/projectile/projectile = new /obj/projectile/soulscythe(get_turf(src))
|
|
projectile.aim_projectile(attacked_atom, src)
|
|
projectile.firer = src
|
|
projectile.fire(null, attacked_atom)
|
|
visible_message(span_danger("[src] fires at [attacked_atom]!"), span_notice("You fire at [attacked_atom]!"))
|
|
playsound(src, 'sound/effects/magic/fireball.ogg', 50, TRUE)
|
|
|
|
/obj/item/soulscythe/proc/slash_target(atom/attacked_atom)
|
|
if(isliving(attacked_atom) && use_blood(10))
|
|
var/mob/living/attacked_mob = attacked_atom
|
|
if(attacked_mob.stat != DEAD)
|
|
give_blood(15)
|
|
attacked_mob.apply_damage(damage = force * (ishostile(attacked_mob) ? 2 : 1), sharpness = SHARP_EDGED, bare_wound_bonus = 5)
|
|
to_chat(attacked_mob, span_userdanger("You're slashed by [src]!"))
|
|
else if((ismachinery(attacked_atom) || isstructure(attacked_atom)) && use_blood(5))
|
|
var/obj/attacked_obj = attacked_atom
|
|
attacked_obj.take_damage(force, BRUTE, MELEE, FALSE)
|
|
else
|
|
return
|
|
COOLDOWN_START(src, attack_cooldown, 1 SECONDS)
|
|
animate(src)
|
|
SpinAnimation(5)
|
|
addtimer(CALLBACK(src, PROC_REF(reset_spin)), 1 SECONDS)
|
|
visible_message(span_danger("[src] slashes [attacked_atom]!"), span_notice("You slash [attacked_atom]!"))
|
|
playsound(src, 'sound/items/weapons/bladeslice.ogg', 50, TRUE)
|
|
do_attack_animation(attacked_atom, ATTACK_EFFECT_SLASH)
|
|
|
|
/obj/item/soulscythe/proc/charge_target(atom/attacked_atom)
|
|
if(charging || !use_blood(30))
|
|
return
|
|
COOLDOWN_START(src, attack_cooldown, 5 SECONDS)
|
|
animate(src)
|
|
charging = TRUE
|
|
visible_message(span_danger("[src] starts charging..."))
|
|
balloon_alert(soul, "you start charging...")
|
|
if(!do_after(soul, 2 SECONDS, target = src, timed_action_flags = IGNORE_TARGET_LOC_CHANGE))
|
|
balloon_alert(soul, "interrupted!")
|
|
return
|
|
visible_message(span_danger("[src] charges at [attacked_atom]!"), span_notice("You charge at [attacked_atom]!"))
|
|
new /obj/effect/temp_visual/mook_dust(get_turf(src))
|
|
playsound(src, 'sound/items/weapons/thudswoosh.ogg', 50, TRUE)
|
|
SpinAnimation(1)
|
|
throwforce *= 2
|
|
throw_at(attacked_atom, 10, 3, soul, FALSE)
|
|
|
|
/obj/item/soulscythe/proc/reset_spin()
|
|
animate(src)
|
|
SpinAnimation(15)
|
|
|
|
/obj/item/soulscythe/Destroy(force)
|
|
soul.ghostize()
|
|
QDEL_NULL(soul)
|
|
. = ..()
|
|
/mob/living/simple_animal/soulscythe
|
|
name = "mysterious spirit"
|
|
maxHealth = 200
|
|
health = 200
|
|
gender = NEUTER
|
|
mob_biotypes = MOB_SPIRIT
|
|
faction = list()
|
|
weather_immunities = list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE)
|
|
/// Blood level, used for movement and abilities in a soulscythe
|
|
var/blood_level = MAX_BLOOD_LEVEL
|
|
|
|
/mob/living/simple_animal/soulscythe/get_status_tab_items()
|
|
. = ..()
|
|
. += "Blood: [blood_level]/[MAX_BLOOD_LEVEL]"
|
|
|
|
/mob/living/simple_animal/soulscythe/Life(seconds_per_tick, times_fired)
|
|
. = ..()
|
|
if(!stat)
|
|
blood_level = min(MAX_BLOOD_LEVEL, blood_level + round(1 * seconds_per_tick))
|
|
|
|
/obj/projectile/soulscythe
|
|
name = "soulslash"
|
|
icon_state = "soulslash"
|
|
armor_flag = MELEE //jokair
|
|
damage = 15
|
|
light_range = 1
|
|
light_power = 1
|
|
light_color = LIGHT_COLOR_BLOOD_MAGIC
|
|
|
|
/obj/projectile/soulscythe/on_hit(atom/target, blocked = 0, pierce_hit)
|
|
if(ishostile(target))
|
|
damage *= 2
|
|
return ..()
|
|
|
|
#undef MAX_BLOOD_LEVEL
|
|
|
|
//Ash Drake: Spectral Blade, Lava Staff, Dragon's Blood
|
|
|
|
/obj/item/melee/ghost_sword
|
|
name = "\improper spectral blade"
|
|
desc = "A rusted and dulled blade. It doesn't look like it'd do much damage. It glows weakly."
|
|
icon = 'icons/obj/weapons/sword.dmi'
|
|
icon_state = "spectral"
|
|
inhand_icon_state = "spectral"
|
|
icon_angle = -45
|
|
lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
|
|
obj_flags = CONDUCTS_ELECTRICITY
|
|
sharpness = SHARP_EDGED
|
|
w_class = WEIGHT_CLASS_BULKY
|
|
force = 1
|
|
throwforce = 1
|
|
hitsound = 'sound/effects/ghost2.ogg'
|
|
block_sound = 'sound/items/weapons/parry.ogg'
|
|
attack_verb_continuous = list("attacks", "slashes", "slices", "tears", "lacerates", "rips", "dices", "rends")
|
|
attack_verb_simple = list("attack", "slash", "slice", "tear", "lacerate", "rip", "dice", "rend")
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
|
var/summon_cooldown = 0
|
|
var/list/mob/dead/observer/spirits
|
|
var/list/alt_continuous = list("stabs", "pierces", "impales")
|
|
var/list/alt_simple = list("stab", "pierce", "impale")
|
|
|
|
/obj/item/melee/ghost_sword/Initialize(mapload)
|
|
. = ..()
|
|
spirits = list()
|
|
START_PROCESSING(SSobj, src)
|
|
SSpoints_of_interest.make_point_of_interest(src)
|
|
alt_continuous = string_list(alt_continuous)
|
|
alt_simple = string_list(alt_simple)
|
|
AddComponent(/datum/component/alternative_sharpness, SHARP_POINTY, alt_continuous, alt_simple)
|
|
AddComponent(\
|
|
/datum/component/butchering, \
|
|
speed = 15 SECONDS, \
|
|
effectiveness = 90, \
|
|
)
|
|
|
|
/obj/item/melee/ghost_sword/Destroy()
|
|
for(var/mob/dead/observer/G in spirits)
|
|
G.RemoveInvisibility(type)
|
|
spirits.Cut()
|
|
STOP_PROCESSING(SSobj, src)
|
|
. = ..()
|
|
|
|
/obj/item/melee/ghost_sword/attack_self(mob/user)
|
|
if(summon_cooldown > world.time)
|
|
to_chat(user, span_warning("You just recently called out for aid. You don't want to annoy the spirits!"))
|
|
return
|
|
to_chat(user, span_notice("You call out for aid, attempting to summon spirits to your side."))
|
|
|
|
notify_ghosts(
|
|
"[user] is raising [user.p_their()] [name], calling for your help!",
|
|
source = user,
|
|
ignore_key = POLL_IGNORE_SPECTRAL_BLADE,
|
|
header = "Spectral blade",
|
|
)
|
|
|
|
summon_cooldown = world.time + 600
|
|
|
|
/obj/item/melee/ghost_sword/process()
|
|
ghost_check()
|
|
|
|
/obj/item/melee/ghost_sword/proc/ghost_check()
|
|
var/ghost_counter = 0
|
|
var/turf/T = get_turf(src)
|
|
var/list/contents = T.get_all_contents()
|
|
var/mob/dead/observer/current_spirits = list()
|
|
for(var/thing in contents)
|
|
var/atom/A = thing
|
|
A.transfer_observers_to(src)
|
|
for(var/i in orbiters?.orbiter_list)
|
|
if(!isobserver(i))
|
|
continue
|
|
var/mob/dead/observer/G = i
|
|
ghost_counter++
|
|
G.SetInvisibility(INVISIBILITY_NONE, id=type, priority=INVISIBILITY_PRIORITY_BASIC_ANTI_INVISIBILITY)
|
|
current_spirits |= G
|
|
for(var/mob/dead/observer/G in spirits - current_spirits)
|
|
G.RemoveInvisibility(type)
|
|
spirits = current_spirits
|
|
return ghost_counter
|
|
|
|
/obj/item/melee/ghost_sword/attack(mob/living/target, mob/living/carbon/human/user)
|
|
force = 0
|
|
var/ghost_counter = ghost_check()
|
|
force = clamp((ghost_counter * 4), 0, 75)
|
|
user.visible_message(span_danger("[user] strikes with the force of [ghost_counter] vengeful spirits!"))
|
|
..()
|
|
|
|
/obj/item/melee/ghost_sword/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
|
|
var/ghost_counter = ghost_check()
|
|
final_block_chance += clamp((ghost_counter * 5), 0, 75)
|
|
owner.visible_message(span_danger("[owner] is protected by a ring of [ghost_counter] ghosts!"))
|
|
return ..()
|
|
|
|
/obj/item/dragons_blood
|
|
name = "bottle of dragons blood"
|
|
desc = "You're not actually going to drink this, are you?"
|
|
icon = 'icons/obj/mining_zones/artefacts.dmi'
|
|
icon_state = "vial"
|
|
|
|
/obj/item/dragons_blood/attack_self(mob/living/carbon/human/user)
|
|
if(!istype(user))
|
|
return
|
|
|
|
var/mob/living/carbon/human/consumer = user
|
|
var/random = rand(2,4) //SKYRAT EDIT - Commenting out #1 because it makes people invisible.
|
|
|
|
switch(random)
|
|
if(1)
|
|
to_chat(user, span_danger("Your appearance morphs to that of a very small humanoid ash dragon! You get to look like a freak without the cool abilities."))
|
|
consumer.dna.features = list(
|
|
"mcolor" = "#A02720",
|
|
"tail_lizard" = "Dark Tiger",
|
|
"tail_human" = "None",
|
|
"snout" = "Sharp",
|
|
"horns" = "Curled",
|
|
"ears" = "None",
|
|
"wings" = "None",
|
|
"frills" = "None",
|
|
"spines" = "Long",
|
|
"lizard_markings" = "Dark Tiger Body",
|
|
"legs" = DIGITIGRADE_LEGS,
|
|
)
|
|
consumer.eye_color_left = "#FEE5A3"
|
|
consumer.eye_color_right = "#FEE5A3"
|
|
consumer.set_species(/datum/species/lizard)
|
|
if(2)
|
|
to_chat(user, span_danger("Your flesh begins to melt! Miraculously, you seem fine otherwise."))
|
|
consumer.set_species(/datum/species/skeleton)
|
|
if(3)
|
|
to_chat(user, span_danger("Power courses through you! You can now shift your form at will."))
|
|
var/datum/action/cooldown/spell/shapeshift/dragon/dragon_shapeshift = new(user.mind || user)
|
|
dragon_shapeshift.Grant(user)
|
|
if(4)
|
|
to_chat(user, span_danger("You feel like you could walk straight through lava now."))
|
|
ADD_TRAIT(user, TRAIT_LAVA_IMMUNE, type)
|
|
|
|
playsound(user,'sound/items/drink.ogg', 30, TRUE)
|
|
qdel(src)
|
|
|
|
/obj/item/lava_staff
|
|
name = "staff of lava"
|
|
desc = "The ability to fill the emergency shuttle with lava. What more could you want out of life?"
|
|
icon_state = "lavastaff"
|
|
inhand_icon_state = "lavastaff"
|
|
icon_angle = -45
|
|
lefthand_file = 'icons/mob/inhands/weapons/staves_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/weapons/staves_righthand.dmi'
|
|
icon = 'icons/obj/weapons/guns/magic.dmi'
|
|
slot_flags = ITEM_SLOT_BACK
|
|
w_class = WEIGHT_CLASS_NORMAL
|
|
force = 18
|
|
damtype = BURN
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
|
attack_verb_continuous = list("sears", "clubs", "burn")
|
|
attack_verb_simple = list("sear", "club", "burn")
|
|
hitsound = 'sound/items/weapons/sear.ogg'
|
|
var/turf_type = /turf/open/lava/smooth/weak
|
|
var/transform_string = "lava"
|
|
var/reset_turf_type = /turf/open/misc/asteroid/basalt
|
|
var/reset_string = "basalt"
|
|
var/create_cooldown = 10 SECONDS
|
|
var/create_delay = 3 SECONDS
|
|
var/reset_cooldown = 5 SECONDS
|
|
var/timer = 0
|
|
var/static/list/banned_turfs = typecacheof(list(/turf/open/space, /turf/closed))
|
|
|
|
/obj/item/lava_staff/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
if(interacting_with.atom_storage || SHOULD_SKIP_INTERACTION(interacting_with, src, user))
|
|
return NONE
|
|
return ranged_interact_with_atom(interacting_with, user, modifiers)
|
|
|
|
/obj/item/lava_staff/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
if(timer > world.time)
|
|
return NONE
|
|
if(is_type_in_typecache(interacting_with, banned_turfs))
|
|
return NONE
|
|
if(!(interacting_with in view(user.client.view, get_turf(user))))
|
|
return NONE
|
|
var/turf/open/T = get_turf(interacting_with)
|
|
if(!istype(T))
|
|
return NONE
|
|
if(!islava(T))
|
|
var/obj/effect/temp_visual/lavastaff/L = new /obj/effect/temp_visual/lavastaff(T)
|
|
L.alpha = 0
|
|
animate(L, alpha = 255, time = create_delay)
|
|
user.visible_message(span_danger("[user] points [src] at [T]!"))
|
|
timer = world.time + create_delay + 1
|
|
if(do_after(user, create_delay, target = T))
|
|
var/old_name = T.name
|
|
if(T.TerraformTurf(turf_type, flags = CHANGETURF_INHERIT_AIR))
|
|
user.visible_message(span_danger("[user] turns \the [old_name] into [transform_string]!"))
|
|
message_admins("[ADMIN_LOOKUPFLW(user)] fired the lava staff at [ADMIN_VERBOSEJMP(T)]")
|
|
user.log_message("fired the lava staff at [AREACOORD(T)].", LOG_ATTACK)
|
|
timer = world.time + create_cooldown
|
|
playsound(T,'sound/effects/magic/fireball.ogg', 200, TRUE)
|
|
else
|
|
timer = world.time
|
|
qdel(L)
|
|
else
|
|
var/old_name = T.name
|
|
if(T.TerraformTurf(reset_turf_type, flags = CHANGETURF_INHERIT_AIR))
|
|
user.visible_message(span_danger("[user] turns \the [old_name] into [reset_string]!"))
|
|
timer = world.time + reset_cooldown
|
|
playsound(T,'sound/effects/magic/fireball.ogg', 200, TRUE)
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
/obj/effect/temp_visual/lavastaff
|
|
icon_state = "lavastaff_warn"
|
|
duration = 50
|
|
|
|
/turf/open/lava/smooth/weak
|
|
lava_damage = 10
|
|
lava_firestacks = 10
|
|
temperature_damage = 2500
|
|
|
|
//Blood-Drunk Miner: Cleaving Saw
|
|
|
|
|
|
/obj/item/melee/cleaving_saw
|
|
name = "cleaving saw"
|
|
desc = "This saw, effective at drawing the blood of beasts, transforms into a long cleaver that makes use of centrifugal force."
|
|
icon = 'icons/obj/mining_zones/artefacts.dmi'
|
|
lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/64x64_righthand.dmi'
|
|
icon_state = "cleaving_saw"
|
|
inhand_icon_state = "cleaving_saw"
|
|
worn_icon_state = "cleaving_saw"
|
|
attack_verb_continuous = list("attacks", "saws", "slices", "tears", "lacerates", "rips", "dices", "cuts")
|
|
attack_verb_simple = list("attack", "saw", "slice", "tear", "lacerate", "rip", "dice", "cut")
|
|
force = 12
|
|
throwforce = 20
|
|
inhand_x_dimension = 64
|
|
inhand_y_dimension = 64
|
|
slot_flags = ITEM_SLOT_BELT
|
|
hitsound = 'sound/items/weapons/bladeslice.ogg'
|
|
w_class = WEIGHT_CLASS_BULKY
|
|
sharpness = SHARP_EDGED
|
|
/// List of factions we deal bonus damage to
|
|
var/list/nemesis_factions = list(FACTION_MINING, FACTION_BOSS)
|
|
/// Amount of damage we deal to the above factions
|
|
var/faction_bonus_force = 30
|
|
/// Whether the cleaver is actively AoE swiping something.
|
|
var/swiping = FALSE
|
|
/// Amount of bleed stacks gained per hit
|
|
var/bleed_stacks_per_hit = 3
|
|
/// Force when the saw is opened.
|
|
var/open_force = 20
|
|
/// Throwforce when the saw is opened.
|
|
var/open_throwforce = 20
|
|
|
|
/obj/item/melee/cleaving_saw/Initialize(mapload)
|
|
. = ..()
|
|
AddComponent( \
|
|
/datum/component/transforming, \
|
|
transform_cooldown_time = (CLICK_CD_MELEE * 0.25), \
|
|
force_on = open_force, \
|
|
throwforce_on = open_throwforce, \
|
|
sharpness_on = sharpness, \
|
|
hitsound_on = hitsound, \
|
|
w_class_on = w_class, \
|
|
attack_verb_continuous_on = list("cleaves", "swipes", "slashes", "chops"), \
|
|
attack_verb_simple_on = list("cleave", "swipe", "slash", "chop"), \
|
|
)
|
|
RegisterSignal(src, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform))
|
|
|
|
/obj/item/melee/cleaving_saw/examine(mob/user)
|
|
. = ..()
|
|
. += span_notice("It is [HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) ? "open, will cleave enemies in a wide arc and deal additional damage to fauna":"closed, and can be used for rapid consecutive attacks that cause fauna to bleed"].")
|
|
. += span_notice("Both modes will build up existing bleed effects, doing a burst of high damage if the bleed is built up high enough.")
|
|
. += span_notice("Transforming it immediately after an attack causes the next attack to come out faster.")
|
|
|
|
/obj/item/melee/cleaving_saw/suicide_act(mob/living/user)
|
|
user.visible_message(span_suicide("[user] is [HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) ? "closing [src] on [user.p_their()] neck" : "opening [src] into [user.p_their()] chest"]! It looks like [user.p_theyre()] trying to commit suicide!"))
|
|
attack_self(user)
|
|
return BRUTELOSS
|
|
|
|
/obj/item/melee/cleaving_saw/melee_attack_chain(mob/user, atom/target, params)
|
|
. = ..()
|
|
if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
|
|
user.changeNext_move(CLICK_CD_MELEE * 0.5) //when closed, it attacks very rapidly
|
|
|
|
/obj/item/melee/cleaving_saw/attack(mob/living/target, mob/living/carbon/human/user)
|
|
var/is_open = HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE)
|
|
if(!is_open || swiping || !target.density || get_turf(target) == get_turf(user))
|
|
if(!is_open)
|
|
faction_bonus_force = 0
|
|
var/is_nemesis_faction = FALSE
|
|
for(var/found_faction in target.faction)
|
|
if(found_faction in nemesis_factions)
|
|
is_nemesis_faction = TRUE
|
|
force += faction_bonus_force
|
|
nemesis_effects(user, target)
|
|
break
|
|
. = ..()
|
|
if(is_nemesis_faction)
|
|
force -= faction_bonus_force
|
|
if(!is_open)
|
|
faction_bonus_force = initial(faction_bonus_force)
|
|
else
|
|
var/turf/user_turf = get_turf(user)
|
|
var/dir_to_target = get_dir(user_turf, get_turf(target))
|
|
swiping = TRUE
|
|
var/static/list/cleaving_saw_cleave_angles = list(0, -45, 45) //so that the animation animates towards the target clicked and not towards a side target
|
|
for(var/i in cleaving_saw_cleave_angles)
|
|
var/turf/turf = get_step(user_turf, turn(dir_to_target, i))
|
|
for(var/mob/living/living_target in turf)
|
|
if(user.Adjacent(living_target) && living_target.body_position != LYING_DOWN)
|
|
melee_attack_chain(user, living_target)
|
|
swiping = FALSE
|
|
|
|
/*
|
|
* If we're attacking [target]s in our nemesis list, apply unique effects.
|
|
*
|
|
* user - the mob attacking with the saw
|
|
* target - the mob being attacked
|
|
*/
|
|
/obj/item/melee/cleaving_saw/proc/nemesis_effects(mob/living/user, mob/living/target)
|
|
if(istype(target, /mob/living/simple_animal/hostile/asteroid/elite))
|
|
return
|
|
var/datum/status_effect/stacking/saw_bleed/existing_bleed = target.has_status_effect(/datum/status_effect/stacking/saw_bleed)
|
|
if(existing_bleed)
|
|
existing_bleed.add_stacks(bleed_stacks_per_hit)
|
|
else
|
|
target.apply_status_effect(/datum/status_effect/stacking/saw_bleed, bleed_stacks_per_hit)
|
|
|
|
/*
|
|
* Signal proc for [COMSIG_TRANSFORMING_ON_TRANSFORM].
|
|
*
|
|
* Gives feedback and makes the nextmove after transforming much quicker.
|
|
*/
|
|
/obj/item/melee/cleaving_saw/proc/on_transform(obj/item/source, mob/user, active)
|
|
SIGNAL_HANDLER
|
|
|
|
user.changeNext_move(CLICK_CD_MELEE * 0.25)
|
|
if(user)
|
|
balloon_alert(user, "[active ? "opened" : "closed"] [src]")
|
|
playsound(src, 'sound/effects/magic/clockwork/fellowship_armory.ogg', 35, TRUE, frequency = 90000 - (active * 30000))
|
|
return COMPONENT_NO_DEFAULT_MESSAGE
|
|
|
|
//Legion: Staff of Storms
|
|
|
|
/obj/item/storm_staff
|
|
name = "staff of storms"
|
|
desc = "An ancient staff retrieved from the remains of Legion. The wind stirs as you move it."
|
|
icon_state = "staffofstorms"
|
|
inhand_icon_state = "staffofstorms"
|
|
icon_angle = -45
|
|
icon = 'icons/obj/weapons/guns/magic.dmi'
|
|
lefthand_file = 'icons/mob/inhands/weapons/staves_lefthand.dmi'
|
|
righthand_file = 'icons/mob/inhands/weapons/staves_righthand.dmi'
|
|
slot_flags = ITEM_SLOT_BACK
|
|
w_class = WEIGHT_CLASS_BULKY
|
|
force = 20
|
|
damtype = BURN
|
|
hitsound = 'sound/items/weapons/taserhit.ogg'
|
|
wound_bonus = -30
|
|
bare_wound_bonus = 20
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
|
var/max_thunder_charges = 3
|
|
var/thunder_charges = 3
|
|
var/thunder_charge_time = 15 SECONDS
|
|
var/static/list/excluded_areas = list(/area/space)
|
|
var/list/targeted_turfs = list()
|
|
|
|
/obj/item/storm_staff/examine(mob/user)
|
|
. = ..()
|
|
. += span_notice("It has [thunder_charges] charges remaining.")
|
|
. += span_notice("Use it in hand to dispel storms.")
|
|
. += span_notice("Use it on targets to summon thunderbolts from the sky.")
|
|
. += span_notice("The thunderbolts are boosted if in an area with weather effects.")
|
|
|
|
/obj/item/storm_staff/attack_self(mob/user)
|
|
var/area/user_area = get_area(user)
|
|
var/turf/user_turf = get_turf(user)
|
|
if(!user_area || !user_turf || (is_type_in_list(user_area, excluded_areas)))
|
|
to_chat(user, span_warning("Something is preventing you from using the staff here."))
|
|
return
|
|
var/datum/weather/affected_weather
|
|
for(var/datum/weather/weather as anything in SSweather.processing)
|
|
if((user_turf.z in weather.impacted_z_levels) && ispath(user_area.type, weather.area_type))
|
|
affected_weather = weather
|
|
break
|
|
if(!affected_weather)
|
|
return
|
|
if(affected_weather.stage == END_STAGE)
|
|
balloon_alert(user, "already ended!")
|
|
return
|
|
if(affected_weather.stage == WIND_DOWN_STAGE)
|
|
balloon_alert(user, "already ending!")
|
|
return
|
|
balloon_alert(user, "you hold the staff up...")
|
|
if(!do_after(user, 3 SECONDS, target = src))
|
|
balloon_alert(user, "interrupted!")
|
|
return
|
|
user.visible_message(span_warning("[user] holds [src] skywards as an orange beam travels into the sky!"), \
|
|
span_notice("You hold [src] skyward, dispelling the storm!"))
|
|
playsound(user, 'sound/effects/magic/staff_change.ogg', 200, FALSE)
|
|
var/old_color = user.color
|
|
user.color = list(340/255, 240/255, 0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0)
|
|
var/old_transform = user.transform
|
|
user.transform *= 1.2
|
|
animate(user, color = old_color, transform = old_transform, time = 1 SECONDS)
|
|
affected_weather.wind_down()
|
|
user.log_message("has dispelled a storm at [AREACOORD(user_turf)].", LOG_GAME)
|
|
|
|
/obj/item/storm_staff/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
|
|
return thunder_blast(interacting_with, user) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
|
|
|
|
/obj/item/storm_staff/afterattack(atom/target, mob/user, click_parameters)
|
|
thunder_blast(target, user)
|
|
|
|
/obj/item/storm_staff/proc/thunder_blast(atom/target, mob/user)
|
|
if(!thunder_charges)
|
|
balloon_alert(user, "needs to charge!")
|
|
return FALSE
|
|
var/turf/target_turf = get_turf(target)
|
|
var/area/target_area = get_area(target)
|
|
if(!target_turf || !target_area || (is_type_in_list(target_area, excluded_areas)))
|
|
balloon_alert(user, "can't bolt here!")
|
|
return FALSE
|
|
if(target_turf in targeted_turfs)
|
|
balloon_alert(user, "already targeted!")
|
|
return FALSE
|
|
if(HAS_TRAIT(user, TRAIT_PACIFISM))
|
|
balloon_alert(user, "you don't want to harm!")
|
|
return FALSE
|
|
var/power_boosted = FALSE
|
|
for(var/datum/weather/weather as anything in SSweather.processing)
|
|
if(weather.stage != MAIN_STAGE)
|
|
continue
|
|
if((target_turf.z in weather.impacted_z_levels) && ispath(target_area.type, weather.area_type))
|
|
power_boosted = TRUE
|
|
break
|
|
playsound(src, 'sound/effects/magic/lightningshock.ogg', 10, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
|
|
targeted_turfs += target_turf
|
|
balloon_alert(user, "you aim at [target_turf]...")
|
|
new /obj/effect/temp_visual/telegraphing/thunderbolt(target_turf)
|
|
addtimer(CALLBACK(src, PROC_REF(throw_thunderbolt), target_turf, power_boosted), 1.5 SECONDS)
|
|
thunder_charges--
|
|
addtimer(CALLBACK(src, PROC_REF(recharge)), thunder_charge_time)
|
|
user.log_message("fired the staff of storms at [AREACOORD(target_turf)].", LOG_ATTACK)
|
|
return TRUE
|
|
|
|
/obj/item/storm_staff/proc/recharge(mob/user)
|
|
thunder_charges = min(thunder_charges + 1, max_thunder_charges)
|
|
playsound(src, 'sound/effects/magic/charge.ogg', 10, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
|
|
|
|
/obj/item/storm_staff/proc/throw_thunderbolt(turf/target, boosted)
|
|
targeted_turfs -= target
|
|
new /obj/effect/temp_visual/thunderbolt(target)
|
|
var/list/affected_turfs = list(target)
|
|
if(boosted)
|
|
for(var/direction in GLOB.alldirs)
|
|
var/turf_to_add = get_step(target, direction)
|
|
if(!turf_to_add)
|
|
continue
|
|
affected_turfs += turf_to_add
|
|
for(var/turf/turf as anything in affected_turfs)
|
|
new /obj/effect/temp_visual/electricity(turf)
|
|
for(var/mob/living/hit_mob in turf)
|
|
to_chat(hit_mob, span_userdanger("You've been struck by lightning!"))
|
|
hit_mob.electrocute_act(15 * (isanimal_or_basicmob(hit_mob) ? 3 : 1) * (turf == target ? 2 : 1) * (boosted ? 2 : 1), src, flags = SHOCK_TESLA|SHOCK_NOSTUN)
|
|
|
|
for(var/obj/hit_thing in turf)
|
|
hit_thing.take_damage(20, BURN, ENERGY, FALSE)
|
|
playsound(target, 'sound/effects/magic/lightningbolt.ogg', 100, TRUE)
|
|
target.visible_message(span_danger("A thunderbolt strikes [target]!"))
|
|
explosion(target, light_impact_range = (boosted ? 1 : 0), flame_range = (boosted ? 2 : 1), silent = TRUE)
|
|
|
|
|
|
/datum/action/innate/brain_undeployment
|
|
name = "Disconnect from shell"
|
|
desc = "Stop controlling your shell and resume normal core operations."
|
|
button_icon = 'icons/mob/actions/actions_AI.dmi'
|
|
button_icon_state = "ai_core"
|
|
|
|
/datum/action/innate/brain_undeployment/Trigger(trigger_flags)
|
|
if(!..())
|
|
return FALSE
|
|
var/obj/item/organ/brain/cybernetic/ai/shell_to_disconnect = owner.get_organ_by_type(/obj/item/organ/brain/cybernetic/ai)
|
|
|
|
shell_to_disconnect.undeploy()
|
|
return TRUE
|
|
|
|
/obj/item/organ/brain/cybernetic/ai
|
|
name = "AI-uplink brain"
|
|
desc = "Can be inserted into a body with NO ORGANIC INTERNAL ORGANS (robotic organs only) to allow AIs to control it. Comes with its own health sensors beacon. MUST be a humanoid or bad things happen to the consciousness."
|
|
can_smoothen_out = FALSE
|
|
/// if connected, our AI
|
|
var/mob/living/silicon/ai/mainframe
|
|
/// action for undeployment
|
|
var/datum/action/innate/brain_undeployment/undeployment_action = new
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/Initialize(mapload)
|
|
. = ..()
|
|
AddElement(/datum/element/noticable_organ, "%PRONOUN_Their eyes move with machine precision, their expression completely blank.")
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/Destroy()
|
|
. = ..()
|
|
undeploy()
|
|
mainframe = null
|
|
QDEL_NULL(undeployment_action)
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/on_mob_insert(mob/living/carbon/brain_owner, special, movement_flags)
|
|
. = ..()
|
|
brain_owner.add_traits(list(HUMAN_SENSORS_VISIBLE_WITHOUT_SUIT, TRAIT_NO_MINDSWAP, TRAIT_CORPSELOCKED), REF(src))
|
|
update_med_hud_status(brain_owner)
|
|
RegisterSignal(brain_owner, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(update_med_hud_status))
|
|
RegisterSignal(brain_owner, COMSIG_CLICK, PROC_REF(owner_clicked))
|
|
RegisterSignal(brain_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item))
|
|
RegisterSignal(brain_owner, COMSIG_MOB_MIND_BEFORE_MIDROUND_ROLL, PROC_REF(cancel_rolls))
|
|
RegisterSignals(brain_owner, list(COMSIG_QDELETING, COMSIG_LIVING_PRE_WABBAJACKED), PROC_REF(undeploy))
|
|
RegisterSignal(brain_owner, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_organ_gain))
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags)
|
|
undeploy()
|
|
. = ..()
|
|
organ_owner.remove_traits(list(HUMAN_SENSORS_VISIBLE_WITHOUT_SUIT, TRAIT_NO_MINDSWAP, TRAIT_CORPSELOCKED), REF(src))
|
|
UnregisterSignal(organ_owner, list(COMSIG_LIVING_HEALTH_UPDATE, COMSIG_CLICK, COMSIG_MOB_GET_STATUS_TAB_ITEMS, COMSIG_MOB_MIND_BEFORE_MIDROUND_ROLL, COMSIG_QDELETING, COMSIG_LIVING_PRE_WABBAJACKED))
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/cancel_rolls(mob/living/source, datum/mind/mind, datum/antagonist/antagonist)
|
|
SIGNAL_HANDLER
|
|
if(ispath(antagonist, /datum/antagonist/malf_ai))
|
|
return
|
|
return CANCEL_ROLL
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/get_status_tab_item(mob/living/source, list/items)
|
|
SIGNAL_HANDLER
|
|
if(!mainframe)
|
|
return
|
|
items += mainframe.get_status_tab_items()
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/update_med_hud_status(mob/living/mob_parent)
|
|
SIGNAL_HANDLER
|
|
var/image/holder = mob_parent.hud_list?[STATUS_HUD]
|
|
if(isnull(holder))
|
|
return
|
|
var/icon/size_check = icon(mob_parent.icon, mob_parent.icon_state, mob_parent.dir)
|
|
holder.pixel_y = size_check.Height() - ICON_SIZE_Y
|
|
if(mob_parent.stat == DEAD || HAS_TRAIT(mob_parent, TRAIT_FAKEDEATH) || isnull(mainframe))
|
|
holder.icon_state = "huddead2"
|
|
holder.pixel_x = -8 // new icon states? nuh uh
|
|
else
|
|
holder.icon_state = "hudtrackingai"
|
|
holder.pixel_x = -16
|
|
|
|
// no thoughts only wifi
|
|
/obj/item/organ/brain/cybernetic/ai/can_gain_trauma(datum/brain_trauma/trauma, resilience, natural_gain = FALSE)
|
|
return FALSE
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/owner_clicked(datum/source, atom/location, control, params, mob/user)
|
|
SIGNAL_HANDLER
|
|
if(!isAI(user))
|
|
return
|
|
var/list/lines = list()
|
|
lines += span_bold("[owner]")
|
|
lines += "Target is currently [!HAS_TRAIT(owner, TRAIT_INCAPACITATED) ? "functional" : "incapacitated"]"
|
|
lines += "Estimated organic/inorganic integrity: [owner.health]"
|
|
if(mainframe)
|
|
lines += span_warning("Already occupied by another digital entity.")
|
|
else if(!is_sufficiently_augmented())
|
|
lines += span_warning("Organic organs detected. Robotic organs only, cannot take over.")
|
|
else
|
|
lines += "<a href='byond://?src=[REF(src)];ai_take_control=[REF(user)]'>[span_boldnotice("Take control?")]</a><br>"
|
|
|
|
to_chat(user, boxed_message(jointext(lines, "\n")), type = MESSAGE_TYPE_INFO)
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/Topic(href, href_list)
|
|
..()
|
|
if(!href_list["ai_take_control"] || !is_sufficiently_augmented() || mainframe)
|
|
return
|
|
var/mob/living/silicon/ai/AI = locate(href_list["ai_take_control"]) in GLOB.silicon_mobs
|
|
if(isnull(AI))
|
|
return
|
|
if(AI.controlled_equipment)
|
|
to_chat(AI, span_warning("You are already loaded into an onboard computer!"))
|
|
return
|
|
if(!GLOB.cameranet.checkCameraVis(owner))
|
|
to_chat(AI, span_warning("Target is no longer near active cameras."))
|
|
return
|
|
if(!isturf(AI.loc))
|
|
to_chat(AI, span_warning("You aren't in your core!"))
|
|
return
|
|
|
|
RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(undeploy))
|
|
AI.deployed_shell = owner
|
|
deploy_init(AI)
|
|
ADD_TRAIT(AI.mind, TRAIT_UNCONVERTABLE, REF(src))
|
|
ADD_TRAIT(AI, TRAIT_MIND_TEMPORARILY_GONE, REF(src))
|
|
AI.mind.transfer_to(owner)
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/deploy_init(mob/living/silicon/ai/AI)
|
|
//todo camera maybe
|
|
mainframe = AI
|
|
RegisterSignal(AI, COMSIG_QDELETING, PROC_REF(ai_deleted))
|
|
undeployment_action.Grant(owner)
|
|
update_med_hud_status(owner)
|
|
to_chat(owner, span_boldbig("You are still considered a silicon/cyborg/AI. Follow your laws."))
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/undeploy(datum/source)
|
|
SIGNAL_HANDLER
|
|
if(!owner?.mind || !mainframe)
|
|
return
|
|
UnregisterSignal(owner, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING))
|
|
UnregisterSignal(mainframe, COMSIG_QDELETING)
|
|
mainframe.redeploy_action.Remove(mainframe)
|
|
mainframe.redeploy_action.last_used_shell = null
|
|
owner.mind.transfer_to(mainframe)
|
|
mainframe.deployed_shell = null
|
|
undeployment_action.Remove(owner)
|
|
if(mainframe.laws)
|
|
mainframe.laws.show_laws(mainframe)
|
|
if(mainframe.eyeobj)
|
|
mainframe.eyeobj.setLoc(loc)
|
|
REMOVE_TRAIT(mainframe.mind, TRAIT_UNCONVERTABLE, REF(src))
|
|
REMOVE_TRAIT(mainframe, TRAIT_MIND_TEMPORARILY_GONE, REF(src))
|
|
mainframe = null
|
|
update_med_hud_status(owner)
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/is_sufficiently_augmented()
|
|
var/mob/living/carbon/carb_owner = owner
|
|
. = TRUE
|
|
if(!istype(carb_owner))
|
|
return
|
|
for(var/obj/item/organ/organ as anything in carb_owner.organs)
|
|
if(organ.organ_flags & ORGAN_EXTERNAL)
|
|
continue
|
|
if(!IS_ROBOTIC_ORGAN(organ) && !istype(organ, /obj/item/organ/tongue)) //tongues are not in the exosuit fab and nobody is going to bother to find them so
|
|
return FALSE
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/on_organ_gain(datum/source, obj/item/organ/new_organ, special)
|
|
SIGNAL_HANDLER
|
|
if(!is_sufficiently_augmented())
|
|
to_chat(owner, span_danger("Connection failure. Organics detected."))
|
|
undeploy()
|
|
|
|
/obj/item/organ/brain/cybernetic/ai/proc/ai_deleted(datum/source)
|
|
SIGNAL_HANDLER
|
|
to_chat(owner, span_danger("Your core has been rendered inoperable..."))
|
|
undeploy()
|