Files
Bubberstation/code/modules/power/apc/apc_attack.dm
nikothedude f44adfde1e Refactors vendor tipping and adds 2 new malf modules: Remote vendor tipping, and the ability to roll around and crush anything in your path. (#76635)
## About The Pull Request

Title.

Vendor tipping code is now on /atom/movable, and any movable can fall
over like a vendor does. Things like crits have been moved to
type-specific availability tables, their effects are now held in their
own proc, are now random per crushed item, have probability weights,
etc.

In the process of making this PR I also had to fix another issue, where
a bunch of take_damage() overrides had incorrect args, so that explains
the take_damage changes I made.

Tipping now also attacks any atoms on the target, given they use
integrity.

Adds 2 new malf modules.

1. REMOTE VENDOR TIPPING: A mid-cost and mid-supply module allows malf
AIs to remotely tip a vendor in any of the 8 directions. After 0.5
seconds of delay and a visual indicator (along with other warnings), the
vendor falls over.
1.1. In the process of making this I had to expand a arrow sprite to
have orthogonal directions, which is why you may see the testing dmi
being changed.
2. CORE ROLLING: A mid-cost but low-supply ability that allows the AI to
roll around and crush anything it falls on, including mobs. This has a
5% chance to have a critical hit so it isnt THAT terrible - plus it's
guaranteed to never stunlock. It's real utility lies in the fact the AI
now has limited movement without borgs. Also, the psychological factor.

As a bonus, vendor tipping now uses animate and transforms instead of
replacing matrices.
## Why It's Good For The Game

1. Generifying vendor tipping code is just good, period. It's a very
wacky and silly little piece of code that really doesn't need to be
isolated to vendors exclusively. ANY big and heavy object can fall over
and do a ton of damage.
1.1. Also, adding weights to critical hits is really good, because it
lets things like the headgib finally be a lot less terrifying, as
they're a lot less likely to happen.

2. Remote vendor tipping is a bit of a goofy ability that isn't really
THAT practical but has a chance of catching someone unaware and doing
some serious damage to that person alone.
2.1. Atop of this, vendor tipping isn't that loud of an action as say,
blowing things up, or doing a plasma flood. Even overrides aren't this
silent or a non-giveaway. A vendor falling on someone, though, is a
mundane thing that happens a lot. This is a decent way to assassinate
people before going loud (or at least, damage people) that isn't offered
yet.

4.
3.1. For real though, AIs rolling around is just fucking hilarious. The
ability to move isn't offered right now (which isn't that much of a bad
things), but with sufficiently limited charges (or limits to how many
times you can buy the ability), this can be a funny little t hing that
lets the AI potentially hide somewhere on the sat (or just relatively
close to the sat, such as engineering [it can't go through the
teleporter with this but it can go through transit tubes]) without the
need for borgs.
3.2. Also, it lets the AI sacrifically execute people by blowing up
their brains.
2023-07-27 18:41:10 +00:00

324 lines
12 KiB
Plaintext

/obj/machinery/power/apc/attackby(obj/item/attacking_object, mob/living/user, params)
if(HAS_TRAIT(attacking_object, TRAIT_APC_SHOCKING))
var/metal = 0
var/shock_source = null
metal += LAZYACCESS(attacking_object.custom_materials, GET_MATERIAL_REF(/datum/material/iron))//This prevents wooden rolling pins from shocking the user
if(cell || terminal) //The mob gets shocked by whichever powersource has the most electricity
if(cell && terminal)
shock_source = cell.charge > terminal.powernet.avail ? cell : terminal.powernet
else
shock_source = terminal?.powernet || cell
if(shock_source && metal && (panel_open || opened)) //Now you're cooking with electricity
if(!electrocute_mob(user, shock_source, src, siemens_coeff = 1, dist_check = TRUE))//People with insulated gloves just attack the APC normally. They're just short of magical anyway
return
do_sparks(5, TRUE, src)
user.visible_message(span_notice("[user.name] shoves [attacking_object] into the internal components of [src], erupting into a cascade of sparks!"))
if(shock_source == cell)//If the shock is coming from the cell just fully discharge it, because it's funny
cell.use(cell.charge)
return
if(issilicon(user) && get_dist(src,user) > 1)
return attack_hand(user)
if(istype(attacking_object, /obj/item/stock_parts/cell) && opened)
if(cell)
balloon_alert(user, "cell already installed!")
return
if(machine_stat & MAINT)
balloon_alert(user, "no connector for a cell!")
return
if(!user.transferItemToLoc(attacking_object, src))
return
cell = attacking_object
user.visible_message(span_notice("[user.name] inserts the power cell to [src.name]!"))
balloon_alert(user, "cell inserted")
chargecount = 0
update_appearance()
return
if(attacking_object.GetID())
togglelock(user)
return
if(istype(attacking_object, /obj/item/stack/cable_coil) && opened)
var/turf/host_turf = get_turf(src)
if(!host_turf)
CRASH("attackby on APC when it's not on a turf")
if(host_turf.underfloor_accessibility < UNDERFLOOR_INTERACTABLE)
balloon_alert(user, "remove the floor plating!")
return
if(terminal)
balloon_alert(user, "already wired!")
return
if(!has_electronics)
balloon_alert(user, "no board to wire!")
return
var/obj/item/stack/cable_coil/installing_cable = attacking_object
if(installing_cable.get_amount() < 10)
balloon_alert(user, "need ten lengths of cable!")
return
var/terminal_cable_layer = cable_layer // Default to machine's cable layer
if(LAZYACCESS(params2list(params), RIGHT_CLICK))
var/choice = tgui_input_list(user, "Select Power Input Cable Layer", "Select Cable Layer", GLOB.cable_name_to_layer)
if(isnull(choice))
return
terminal_cable_layer = GLOB.cable_name_to_layer[choice]
user.visible_message(span_notice("[user.name] adds cables to the APC frame."))
balloon_alert(user, "adding cables to the frame...")
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
if(!do_after(user, 20, target = src))
return
if(installing_cable.get_amount() < 10 || !installing_cable)
return
if(terminal || !opened || !has_electronics)
return
var/turf/our_turf = get_turf(src)
var/obj/structure/cable/cable_node = our_turf.get_cable_node(terminal_cable_layer)
if(prob(50) && electrocute_mob(usr, cable_node, cable_node, 1, TRUE))
do_sparks(5, TRUE, src)
return
installing_cable.use(10)
balloon_alert(user, "cables added to the frame")
make_terminal(terminal_cable_layer)
terminal.connect_to_network()
return
if(istype(attacking_object, /obj/item/electronics/apc) && opened)
if(has_electronics)
balloon_alert(user, "there is already a board!")
return
if(machine_stat & BROKEN)
balloon_alert(user, "the frame is damaged!")
return
user.visible_message(span_notice("[user.name] inserts the power control board into [src]."))
balloon_alert(user, "you start to insert the board...")
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
if(!do_after(user, 10, target = src) || has_electronics)
return
has_electronics = APC_ELECTRONICS_INSTALLED
locked = FALSE
balloon_alert(user, "board installed")
qdel(attacking_object)
return
if(istype(attacking_object, /obj/item/electroadaptive_pseudocircuit) && opened)
var/obj/item/electroadaptive_pseudocircuit/pseudocircuit = attacking_object
if(!has_electronics)
if(machine_stat & BROKEN)
balloon_alert(user, "frame is too damaged!")
return
if(!pseudocircuit.adapt_circuit(user, 50))
return
user.visible_message(span_notice("[user] fabricates a circuit and places it into [src]."), \
span_notice("You adapt a power control board and click it into place in [src]'s guts."))
has_electronics = APC_ELECTRONICS_INSTALLED
locked = FALSE
return
if(!cell)
if(machine_stat & MAINT)
balloon_alert(user, "no board for a cell!")
return
if(!pseudocircuit.adapt_circuit(user, 500))
return
var/obj/item/stock_parts/cell/crap/empty/bad_cell = new(src)
bad_cell.forceMove(src)
cell = bad_cell
chargecount = 0
user.visible_message(span_notice("[user] fabricates a weak power cell and places it into [src]."), \
span_warning("Your [pseudocircuit.name] whirrs with strain as you create a weak power cell and place it into [src]!"))
update_appearance()
return
balloon_alert(user, "has both board and cell!")
return
if(istype(attacking_object, /obj/item/wallframe/apc) && opened)
if(!(machine_stat & BROKEN || opened == APC_COVER_REMOVED || atom_integrity < max_integrity)) // There is nothing to repair
balloon_alert(user, "no reason for repairs!")
return
if((machine_stat & BROKEN) && opened == APC_COVER_REMOVED && has_electronics && terminal) // Cover is the only thing broken, we do not need to remove elctronicks to replace cover
user.visible_message(span_notice("[user.name] replaces missing APC's cover."))
balloon_alert(user, "replacing APC's cover...")
if(do_after(user, 20, target = src)) // replacing cover is quicker than replacing whole frame
balloon_alert(user, "cover replaced")
qdel(attacking_object)
update_integrity(30) //needs to be welded to fully repair but can work without
set_machine_stat(machine_stat & ~(BROKEN|MAINT))
opened = APC_COVER_OPENED
update_appearance()
return
if(has_electronics)
balloon_alert(user, "remove the board inside!")
return
user.visible_message(span_notice("[user.name] replaces the damaged APC frame with a new one."))
balloon_alert(user, "replacing damaged frame...")
if(do_after(user, 50, target = src))
balloon_alert(user, "replaced frame")
qdel(attacking_object)
set_machine_stat(machine_stat & ~BROKEN)
atom_integrity = max_integrity
if(opened == APC_COVER_REMOVED)
opened = APC_COVER_OPENED
update_appearance()
return
if(panel_open && !opened && is_wire_tool(attacking_object))
wires.interact(user)
return
return ..()
/obj/machinery/power/apc/attack_hand_secondary(mob/user, list/modifiers)
. = ..()
if(!can_interact(user))
return
if(!user.can_perform_action(src, ALLOW_SILICON_REACH) || !isturf(loc))
return
if(!ishuman(user))
return
var/mob/living/carbon/human/apc_interactor = user
var/obj/item/organ/internal/stomach/ethereal/maybe_ethereal_stomach = apc_interactor.get_organ_slot(ORGAN_SLOT_STOMACH)
if(!istype(maybe_ethereal_stomach))
togglelock(user)
else
if(maybe_ethereal_stomach.crystal_charge >= ETHEREAL_CHARGE_NORMAL)
togglelock(user)
ethereal_interact(user, modifiers)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/// Special behavior for when an ethereal interacts with an APC.
/obj/machinery/power/apc/proc/ethereal_interact(mob/living/user, list/modifiers)
if(!ishuman(user))
return
var/mob/living/carbon/human/ethereal = user
var/obj/item/organ/internal/stomach/maybe_stomach = ethereal.get_organ_slot(ORGAN_SLOT_STOMACH)
// how long we wanna wait before we show the balloon alert. don't want it to be very long in case the ethereal wants to opt-out of doing that action, just long enough to where it doesn't collide with previously queued balloon alerts.
var/alert_timer_duration = 0.75 SECONDS
if(!istype(maybe_stomach, /obj/item/organ/internal/stomach/ethereal))
return
var/charge_limit = ETHEREAL_CHARGE_DANGEROUS - APC_POWER_GAIN
var/obj/item/organ/internal/stomach/ethereal/stomach = maybe_stomach
if(!((stomach?.drain_time < world.time) && LAZYACCESS(modifiers, RIGHT_CLICK)))
return
if(ethereal.combat_mode)
if(cell.charge <= (cell.maxcharge / 2)) // ethereals can't drain APCs under half charge, this is so that they are forced to look to alternative power sources if the station is running low
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, balloon_alert), ethereal, "safeties prevent draining!"), alert_timer_duration)
return
if(stomach.crystal_charge > charge_limit)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, balloon_alert), ethereal, "charge is full!"), alert_timer_duration)
return
stomach.drain_time = world.time + APC_DRAIN_TIME
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, balloon_alert), ethereal, "draining power"), alert_timer_duration)
if(do_after(user, APC_DRAIN_TIME, target = src))
if(cell.charge <= (cell.maxcharge / 2) || (stomach.crystal_charge > charge_limit))
return
balloon_alert(ethereal, "received charge")
stomach.adjust_charge(APC_POWER_GAIN)
cell.use(APC_POWER_GAIN)
return
if(cell.charge >= cell.maxcharge - APC_POWER_GAIN)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, balloon_alert), ethereal, "APC can't receive more power!"), alert_timer_duration)
return
if(stomach.crystal_charge < APC_POWER_GAIN)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, balloon_alert), ethereal, "charge is too low!"), alert_timer_duration)
return
stomach.drain_time = world.time + APC_DRAIN_TIME
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, balloon_alert), ethereal, "transfering power"), alert_timer_duration)
if(!do_after(user, APC_DRAIN_TIME, target = src))
return
if((cell.charge >= (cell.maxcharge - APC_POWER_GAIN)) || (stomach.crystal_charge < APC_POWER_GAIN))
balloon_alert(ethereal, "can't transfer power!")
return
if(istype(stomach))
balloon_alert(ethereal, "transfered power")
stomach.adjust_charge(-APC_POWER_GAIN)
cell.give(APC_POWER_GAIN)
else
balloon_alert(ethereal, "can't transfer power!")
// attack with hand - remove cell (if cover open) or interact with the APC
/obj/machinery/power/apc/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
if(opened && (!issilicon(user)))
if(cell)
user.visible_message(span_notice("[user] removes \the [cell] from [src]!"))
balloon_alert(user, "cell removed")
user.put_in_hands(cell)
cell.update_appearance()
cell = null
charging = APC_NOT_CHARGING
update_appearance()
return
if((machine_stat & MAINT) && !opened) //no board; no interface
return
/obj/machinery/power/apc/blob_act(obj/structure/blob/B)
set_broken()
/obj/machinery/power/apc/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armor_penetration = 0)
// APC being at 0 integrity doesnt delete it outright. Combined with take_damage this might cause runtimes.
if(machine_stat & BROKEN && atom_integrity <= 0)
if(sound_effect)
play_attack_sound(damage_amount, damage_type, damage_flag)
return
return ..()
/obj/machinery/power/apc/run_atom_armor(damage_amount, damage_type, damage_flag = 0, attack_dir)
if(machine_stat & BROKEN)
return damage_amount
. = ..()
/obj/machinery/power/apc/atom_break(damage_flag)
. = ..()
if(.)
set_broken()
/obj/machinery/power/apc/proc/can_use(mob/user, loud = 0) //used by attack_hand() and Topic()
if(isAdminGhostAI(user))
return TRUE
if(!user.has_unlimited_silicon_privilege)
return TRUE
var/mob/living/silicon/ai/AI = user
var/mob/living/silicon/robot/robot = user
if(aidisabled || malfhack && istype(malfai) && ((istype(AI) && (malfai != AI && malfai != AI.parent)) || (istype(robot) && (robot in malfai.connected_robots))))
if(!loud)
balloon_alert(user, "it's disabled!")
return FALSE
return TRUE
/obj/machinery/power/apc/proc/set_broken()
if(malfai && operating)
malfai.malf_picker.processing_time = clamp(malfai.malf_picker.processing_time - 10,0,1000)
operating = FALSE
atom_break()
if(occupier)
malfvacate(TRUE)
update()
/obj/machinery/power/apc/proc/shock(mob/user, prb)
if(!prob(prb))
return FALSE
do_sparks(5, TRUE, src)
if(isalien(user))
return FALSE
if(electrocute_mob(user, src, src, 1, TRUE))
return TRUE
else
return FALSE