Files
Bubberstation/code/modules/mining/lavaland/megafauna_loot.dm
SmArtKar 589cf0a904 Refactors how item actions are handled (#89654)
## 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)
```
2025-03-12 16:53:44 -04:00

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()