Files
Bubberstation/code/modules/wiremod/shell/brain_computer_interface.dm
RikuTheKiller 0747099063 Adds a reagent injector component and BCI manipulators to all circuit labs (#71236)
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request

This PR adds a reagent injector component that's exclusive to BCIs.
(Requested to be integrated into BCIs by Mothblocks.)
When outside of a circuit, the component itself stores the reagents.
However, if it's inside of a BCI, the storage is moved to the BCI. The
storage can contain up to 15u of reagents and acts like an open
container. (However, it won't spill even if you throw it, it just acts
like an open container code-wise, don't worry about it.)
You can only have one reagent injector in a circuit. Trying to insert
multiple will give you an error message.
The entire dose is administered at once. (Requirement set by
Mothblocks.)

Please don't try to dispute any of the specific limitations in the
comments as they're out of my control. They're reasonable anyways.

Reagent Injector Input/Output:
Inject (Input Signal) - Administers all reagents currently stored inside
of the BCI into the user.
Injected (Output Signal) - Triggered when reagents are injected. Not
triggered if the reagent storage is empty.

New BCI Input:
Show Charge Meter (Number) - Toggles showing the charge meter action.
(Adds some capacity for stealth.)

Install Detector Outputs: (Added following a comment about having to use
weird workarounds for proper loops.)
Current State (Number) - Outputs 1 if the BCI is implanted and 0 if it's
not.
Installed (Signal) - Triggered when the BCI is implanted into it's user.
Removed (Signal) - Triggered when the BCI is removed from it's user.

This PR also adds BCI manipulation chambers to all currently present
circuit labs. (Solution proposed by Mothblocks.)
Yes I had to do some other mapping changes to allow for this. No I don't
have any mapping experience, why do you ask?

<!-- Describe The Pull Request. Please be sure every change is
documented or this can delay review and even discourage maintainers from
merging your PR! -->

## Why It's Good For The Game

One small step for BCIs, one giant leap for circuit kind. (First
"proper" circuit to human interaction in the entire game!)

This allows for some funky stuff and also makes it less of a pain in the
ass to use BCIs. What's not to love?

<!-- Argue for the merits of your changes and how they benefit the game,
especially if they are controversial and/or far reaching. If you can't
actually explain WHY what you are doing will improve the game, then it
probably isn't good for the game in the first place. -->

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

🆑
add: Added a reagent injector component and BCI manipulators to all
circuit labs. (+ install detector component)
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
2022-11-20 23:17:34 -08:00

535 lines
16 KiB
Plaintext

/obj/item/organ/internal/cyberimp/bci
name = "brain-computer interface"
desc = "An implant that can be placed in a user's head to control circuits using their brain."
icon = 'icons/obj/wiremod.dmi'
icon_state = "bci"
visual = FALSE
zone = BODY_ZONE_HEAD
w_class = WEIGHT_CLASS_TINY
/obj/item/organ/internal/cyberimp/bci/Initialize(mapload)
. = ..()
var/obj/item/integrated_circuit/circuit = new(src)
circuit.add_component(new /obj/item/circuit_component/equipment_action/bci(null, "One"))
AddComponent(/datum/component/shell, list(
new /obj/item/circuit_component/bci_core,
), SHELL_CAPACITY_SMALL, starting_circuit = circuit)
/obj/item/organ/internal/cyberimp/bci/Insert(mob/living/carbon/reciever, special, drop_if_replaced)
. = ..()
// Organs are put in nullspace, but this breaks circuit interactions
forceMove(reciever)
/obj/item/organ/internal/cyberimp/bci/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced = null, filterproof = null, message_range = 7, datum/saymode/saymode = null)
if (owner)
// Otherwise say_dead will be called.
// It's intentional that a circuit for a dead person does not speak from the shell.
if (owner.stat == DEAD)
return
owner.say(message, forced = "circuit speech")
else
return ..()
/obj/item/circuit_component/equipment_action/bci
display_name = "BCI Action"
desc = "Represents an action the user can take when implanted with the brain-computer interface."
required_shells = list(/obj/item/organ/internal/cyberimp/bci)
/// A reference to the action button itself
var/datum/action/innate/bci_action/bci_action
/obj/item/circuit_component/equipment_action/bci/Destroy()
QDEL_NULL(bci_action)
return ..()
/obj/item/circuit_component/equipment_action/bci/register_shell(atom/movable/shell)
. = ..()
var/obj/item/organ/internal/cyberimp/bci/bci = shell
if(istype(bci))
bci_action = new(src)
update_action()
bci.actions += list(bci_action)
/obj/item/circuit_component/equipment_action/bci/unregister_shell(atom/movable/shell)
var/obj/item/organ/internal/cyberimp/bci/bci = shell
if(istype(bci))
bci.actions -= bci_action
QDEL_NULL(bci_action)
return ..()
/obj/item/circuit_component/equipment_action/bci/input_received(datum/port/input/port)
if (!isnull(bci_action))
update_action()
/obj/item/circuit_component/equipment_action/bci/update_action()
bci_action.name = button_name.value
bci_action.button_icon_state = "bci_[replacetextEx(lowertext(icon_options.value), " ", "_")]"
/datum/action/innate/bci_action
name = "Action"
icon_icon = 'icons/mob/actions/actions_items.dmi'
check_flags = AB_CHECK_CONSCIOUS
button_icon_state = "bci_power"
var/obj/item/circuit_component/equipment_action/bci/circuit_component
/datum/action/innate/bci_action/New(obj/item/circuit_component/equipment_action/bci/circuit_component)
..()
src.circuit_component = circuit_component
/datum/action/innate/bci_action/Destroy()
circuit_component.bci_action = null
circuit_component = null
return ..()
/datum/action/innate/bci_action/Activate()
circuit_component.signal.set_output(COMPONENT_SIGNAL)
/obj/item/circuit_component/bci_core
display_name = "BCI Core"
desc = "Controls the core operations of the BCI."
/// A reference to the action button to look at charge/get info
var/datum/action/innate/bci_charge_action/charge_action
var/datum/port/input/message
var/datum/port/input/send_message_signal
var/datum/port/input/show_charge_meter
var/datum/port/output/user_port
var/datum/weakref/user
var/obj/item/organ/internal/cyberimp/bci/bci
/obj/item/circuit_component/bci_core/populate_ports()
message = add_input_port("Message", PORT_TYPE_STRING, trigger = null)
send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL)
show_charge_meter = add_input_port("Show Charge Meter", PORT_TYPE_NUMBER, trigger = PROC_REF(update_charge_action))
user_port = add_output_port("User", PORT_TYPE_ATOM)
/obj/item/circuit_component/bci_core/Destroy()
QDEL_NULL(charge_action)
return ..()
/obj/item/circuit_component/bci_core/proc/update_charge_action()
CIRCUIT_TRIGGER
var/mob/living/carbon/resolved_owner = user?.resolve()
if (show_charge_meter.value)
if (charge_action)
return
charge_action = new(src)
if (resolved_owner)
charge_action.Grant(resolved_owner)
bci.actions += charge_action
else
if (!charge_action)
return
if (resolved_owner)
charge_action.Remove(resolved_owner)
bci.actions -= charge_action
QDEL_NULL(charge_action)
/obj/item/circuit_component/bci_core/register_shell(atom/movable/shell)
bci = shell
show_charge_meter.set_value(TRUE)
RegisterSignal(shell, COMSIG_ORGAN_IMPLANTED, PROC_REF(on_organ_implanted))
RegisterSignal(shell, COMSIG_ORGAN_REMOVED, PROC_REF(on_organ_removed))
/obj/item/circuit_component/bci_core/unregister_shell(atom/movable/shell)
bci = shell
if (charge_action)
var/mob/living/carbon/resolved_owner = user?.resolve()
if (resolved_owner)
charge_action.Remove(resolved_owner)
bci.actions -= charge_action
QDEL_NULL(charge_action)
UnregisterSignal(shell, list(
COMSIG_ORGAN_IMPLANTED,
COMSIG_ORGAN_REMOVED,
))
/obj/item/circuit_component/bci_core/input_received(datum/port/input/port)
var/sent_message = trim(message.value)
if (!sent_message)
return
var/mob/living/carbon/resolved_owner = user?.resolve()
if (isnull(resolved_owner))
return
if (resolved_owner.stat == DEAD)
return
to_chat(resolved_owner, "<i>You hear a strange, robotic voice in your head...</i> \"[span_robot("[html_encode(sent_message)]")]\"")
/obj/item/circuit_component/bci_core/proc/on_organ_implanted(datum/source, mob/living/carbon/owner)
SIGNAL_HANDLER
update_charge_action()
user_port.set_output(owner)
user = WEAKREF(owner)
RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(on_borg_charge))
RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_electrocute))
/obj/item/circuit_component/bci_core/proc/on_organ_removed(datum/source, mob/living/carbon/owner)
SIGNAL_HANDLER
user_port.set_output(null)
user = null
UnregisterSignal(owner, list(
COMSIG_PARENT_EXAMINE,
COMSIG_PROCESS_BORGCHARGER_OCCUPANT,
COMSIG_LIVING_ELECTROCUTE_ACT,
))
/obj/item/circuit_component/bci_core/proc/on_borg_charge(datum/source, amount)
SIGNAL_HANDLER
if (isnull(parent.cell))
return
parent.cell.give(amount)
/obj/item/circuit_component/bci_core/proc/on_electrocute(datum/source, shock_damage, siemens_coefficient, flags)
SIGNAL_HANDLER
if (isnull(parent.cell))
return
if (flags & SHOCK_ILLUSION)
return
parent.cell.give(shock_damage * 2)
to_chat(source, span_notice("You absorb some of the shock into your [parent.name]!"))
/obj/item/circuit_component/bci_core/proc/on_examine(datum/source, mob/mob, list/examine_text)
SIGNAL_HANDLER
if (isobserver(mob))
examine_text += span_notice("[source.p_they(capitalized = TRUE)] [source.p_have()] <a href='?src=[REF(src)];open_bci=1'>\a [parent] implanted in [source.p_them()]</a>.")
/obj/item/circuit_component/bci_core/Topic(href, list/href_list)
..()
if (!isobserver(usr))
return
if (href_list["open_bci"])
parent.attack_ghost(usr)
/datum/action/innate/bci_charge_action
name = "Check BCI Charge"
check_flags = NONE
icon_icon = 'icons/obj/power.dmi'
button_icon_state = "cell"
var/obj/item/circuit_component/bci_core/circuit_component
/datum/action/innate/bci_charge_action/New(obj/item/circuit_component/bci_core/circuit_component)
..()
src.circuit_component = circuit_component
UpdateButtons()
START_PROCESSING(SSobj, src)
/datum/action/innate/bci_charge_action/CreateButton()
var/atom/movable/screen/movable/action_button/button = ..()
button.maptext_x = 2
button.maptext_y = 0
return button
/datum/action/innate/bci_charge_action/Destroy()
circuit_component.charge_action = null
circuit_component = null
STOP_PROCESSING(SSobj, src)
return ..()
/datum/action/innate/bci_charge_action/Trigger(trigger_flags)
var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell
if (isnull(cell))
to_chat(owner, span_boldwarning("[circuit_component.parent] has no power cell."))
else
to_chat(owner, span_info("[circuit_component.parent]'s [cell.name] has <b>[cell.percent()]%</b> charge left."))
to_chat(owner, span_info("You can recharge it by using a cyborg recharging station."))
/datum/action/innate/bci_charge_action/process(delta_time)
UpdateButtons()
/datum/action/innate/bci_charge_action/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE)
. = ..()
if(!.)
return
if(status_only)
return
var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell
button.maptext = cell ? MAPTEXT("[cell.percent()]%") : ""
/obj/machinery/bci_implanter
name = "brain-computer interface manipulation chamber"
desc = "A machine that, when given a brain-computer interface, will implant it into an occupant. Otherwise, will remove any brain-computer interfaces they already have."
circuit = /obj/item/circuitboard/machine/bci_implanter
icon = 'icons/obj/machines/bci_implanter.dmi'
icon_state = "bci_implanter"
base_icon_state = "bci_implanter"
layer = ABOVE_WINDOW_LAYER
anchored = TRUE
density = TRUE
obj_flags = NO_BUILD // Becomes undense when the door is open
var/busy = FALSE
var/busy_icon_state
var/locked = FALSE
var/obj/item/organ/internal/cyberimp/bci/bci_to_implant
COOLDOWN_DECLARE(message_cooldown)
/obj/machinery/bci_implanter/Initialize(mapload)
. = ..()
occupant_typecache = typecacheof(/mob/living/carbon)
/obj/machinery/bci_implanter/on_deconstruction()
drop_stored_bci()
/obj/machinery/bci_implanter/Destroy()
qdel(bci_to_implant)
return ..()
/obj/machinery/bci_implanter/examine(mob/user)
. = ..()
if (isnull(bci_to_implant))
. += span_notice("There is no BCI inserted.")
else
. += span_notice("Right-click to remove current BCI.")
/obj/machinery/bci_implanter/proc/set_busy(status, working_icon)
busy = status
busy_icon_state = working_icon
update_appearance()
/obj/machinery/bci_implanter/update_icon_state()
if (occupant)
icon_state = busy ? busy_icon_state : "[base_icon_state]_occupied"
return ..()
icon_state = "[base_icon_state][state_open ? "_open" : null]"
return ..()
/obj/machinery/bci_implanter/update_overlays()
var/list/overlays = ..()
if ((machine_stat & MAINT) || panel_open)
overlays += "maint"
return overlays
if (machine_stat & (NOPOWER|BROKEN))
return overlays
if (busy || locked)
overlays += "red"
if (locked)
overlays += "bolted"
return overlays
overlays += "green"
return overlays
/obj/machinery/bci_implanter/attack_hand_secondary(mob/user, list/modifiers)
. = ..()
if (. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
return .
if(!user.Adjacent(src))
return
if (locked)
balloon_alert(user, "it's locked!")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if (isnull(bci_to_implant))
balloon_alert(user, "no bci inserted!")
else
user.put_in_hands(bci_to_implant)
balloon_alert(user, "ejected bci")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/bci_implanter/attackby(obj/item/weapon, mob/user, params)
var/obj/item/organ/internal/cyberimp/bci/new_bci = weapon
if (istype(new_bci))
if (!(locate(/obj/item/integrated_circuit) in new_bci))
balloon_alert(user, "bci has no circuit!")
return
var/obj/item/organ/internal/cyberimp/bci/previous_bci_to_implant = bci_to_implant
user.transferItemToLoc(weapon, src)
bci_to_implant = weapon
if (isnull(previous_bci_to_implant))
balloon_alert(user, "inserted bci")
else
balloon_alert(user, "swapped bci")
user.put_in_hands(previous_bci_to_implant)
return
return ..()
/obj/machinery/bci_implanter/attackby_secondary(obj/item/weapon, mob/user, params)
if (!occupant && default_deconstruction_screwdriver(user, icon_state, icon_state, weapon))
update_appearance()
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if (default_pry_open(weapon))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if (default_deconstruction_crowbar(weapon))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/bci_implanter/proc/start_process()
if (machine_stat & (NOPOWER|BROKEN))
return
if ((machine_stat & MAINT) || panel_open)
return
if (!occupant || busy)
return
update_use_power(ACTIVE_POWER_USE)
var/locked_state = locked
locked = TRUE
set_busy(TRUE, "[initial(icon_state)]_raising")
addtimer(CALLBACK(src, PROC_REF(set_busy), TRUE, "[initial(icon_state)]_active"), 1 SECONDS)
addtimer(CALLBACK(src, PROC_REF(set_busy), TRUE, "[initial(icon_state)]_falling"), 2 SECONDS)
addtimer(CALLBACK(src, PROC_REF(complete_process), locked_state), 3 SECONDS)
/obj/machinery/bci_implanter/proc/complete_process(locked_state)
update_use_power(IDLE_POWER_USE)
locked = locked_state
set_busy(FALSE)
var/mob/living/carbon/carbon_occupant = occupant
if (!istype(carbon_occupant))
return
playsound(loc, 'sound/machines/ping.ogg', 30, FALSE)
var/obj/item/organ/internal/cyberimp/bci/bci_organ = carbon_occupant.getorgan(/obj/item/organ/internal/cyberimp/bci)
if (bci_organ)
bci_organ.Remove(carbon_occupant)
if (isnull(bci_to_implant))
say("Occupant's previous brain-computer interface has been transferred to internal storage unit.")
carbon_occupant.transferItemToLoc(bci_organ, src)
bci_to_implant = bci_organ
else
say("Occupant's previous brain-computer interface has been ejected.")
bci_organ.forceMove(drop_location())
else if (!isnull(bci_to_implant))
say("Occupant has been injected with [bci_to_implant].")
bci_to_implant.Insert(carbon_occupant)
/obj/machinery/bci_implanter/open_machine()
if(state_open)
return FALSE
..()
return TRUE
/obj/machinery/bci_implanter/close_machine(mob/living/carbon/user)
if(!state_open)
return FALSE
..()
var/mob/living/carbon/carbon_occupant = occupant
if (istype(occupant))
var/obj/item/organ/internal/cyberimp/bci/bci_organ = carbon_occupant.getorgan(/obj/item/organ/internal/cyberimp/bci)
if (isnull(bci_organ) && isnull(bci_to_implant))
say("No brain-computer interface inserted, and occupant does not have one. Insert a BCI to implant one.")
playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE)
return FALSE
addtimer(CALLBACK(src, PROC_REF(start_process)), 1 SECONDS)
return TRUE
/obj/machinery/bci_implanter/relaymove(mob/living/user, direction)
var/message
if (locked)
message = "it won't budge!"
else if (user.stat != CONSCIOUS)
message = "you don't have the energy!"
if (!isnull(message))
if (COOLDOWN_FINISHED(src, message_cooldown))
COOLDOWN_START(src, message_cooldown, 5 SECONDS)
balloon_alert(user, message)
return
open_machine()
/obj/machinery/bci_implanter/interact(mob/user)
if (state_open)
close_machine(null, user)
return
else if (locked)
balloon_alert(user, "it's locked!")
return
open_machine()
/obj/machinery/bci_implanter/proc/drop_stored_bci()
if (isnull(bci_to_implant))
return
bci_to_implant.forceMove(drop_location())
/obj/machinery/bci_implanter/dump_inventory_contents(list/subset)
// Prevents opening the machine dropping the BCI.
// "dump_contents()" still drops the BCI.
return ..(contents - bci_to_implant)
/obj/machinery/bci_implanter/Exited(atom/movable/gone, direction)
if (gone == bci_to_implant)
bci_to_implant = null
return ..()
/obj/item/circuitboard/machine/bci_implanter
name = "Brain-Computer Interface Manipulation Chamber (Machine Board)"
greyscale_colors = CIRCUIT_COLOR_SCIENCE
build_path = /obj/machinery/bci_implanter
req_components = list(
/obj/item/stock_parts/micro_laser = 2,
/obj/item/stock_parts/manipulator = 1,
)