Files
Bubberstation/code/game/objects/items/devices/laserpointer.dm
John Willard 9ac81e1a64 New station trait job: Human AI (#81681)
## About The Pull Request

This PR does many things, I'll try to explain the basic/background stuff
to the main thing first:

1. Adds a new remote that allows a human to function like an AI. It
controls a fly that will fly around the station slowly, and when it
reaches a machine then the person can interact with it as if they were
an AI. This required changing a lot of silicon/AI checks with one that
also checks for this remote, and some messing with shared ui state.
2. Moves req_access from the obj and bot to ``/atom/movable`` which lets
it be shared between the two, no more copy-paste and one side lacking
features/checks/signals the other has.
3. Adds a check for AI config for AI-related station traits, which was
lacking prior

Now for the good part...
Adds a new station trait that replaces the AI with a Human.
This person is equipped with an AI headset (including Binary), an
advanced camera console, an omni door wand, the machine controller, and
their laws.
They are immune to the SAT's turrets (even if set to target borgs) and
are slow outside of the SAT, mimicing the actions of the AI.

They interact with the world through their advanced camera console,
which allows them to do most AI stuff needed, and the holopad they can
connect to without having to ring first (like Command can).

They are given a paper with the laws they must follow, but since they
are human they are able to bend it. Cyborgs that run the default lawset
are "slaved" to them via an unremovable law 0, so the Human AI can bend
the laws if they really need to (for their own survival n such), and
make the cyborgs obey their commands above laws, but in general this
shouldn't be a frequent occurrence. This does take into account the
unique AI trait, so it's not guaranteed Asimov.

When this station trait rolls, all Intellicards, AI uploads, and AI core
boards are destroyed and are unresearchable. They can be spawned by
admins in-game if necessary. Maybe in the future we can also exclude
Oldstation from this but I haven't really decided.

Extra perks:

Human AI spawns with a Robotic voicebox (unless they are a body purist)
and teleport blocking implant, so they can't use teleporters to bypass
their on-station slowdown.
They also have an infinite laser pointer that can be used to blind
through their camera console. This is unfortunately nerfed from the
recent borg balance PR that removed its stun. This was meant to be the
alternative to no longer being able to permanently lock borgs down like
AIs can (or more than one, for that matter).
They aren't affected by Roburgers, Acid, and Fuel's toxicity.
Bots salute them like they do Beepsky (which is now a trait)
They spawn with SyndEye to replace the AI's tracking ability
They do not have a bank account

### The machine remote

The machine remote has a little fly in it that flies to the machines it
is pointed to, working as the arms and legs of the Human AI. It scans
the machine and punches in the action the AI does, and is how the AI
accesses basically anything. This fly slowly moves from one machine to
the next, and can be recalled with Alt Click.
It works on machines and bots.

### Video (Low quality to fit Github)


https://github.com/tgstation/tgstation/assets/53777086/e16509f8-8bed-42b5-9fbf-7e37165a11e8

## Why It's Good For The Game

I've seen a funny screenshot one day of a person replacing the AI by
using a bunch of door remotes, camera console, crew monitoring console,
and a few other things. I've been thinking about that for a few years
and really wanted to make it official if not easier to make possible,
because it is an incredibly funny interaction.
This makes it a reality, and while they aren't as powerful as regular
AIs, I think it makes for better and funnier in-game moments. With the
same weight as Cargorilla (1), I hope this wouldn't be rolling too often
and ruin rounds, but instead show off the different capabilities that
Humans and AIs can do, to do the job of an AI. You win some you lose
some.

## Changelog

🆑 JohnFulpWillard, Tattax
add: Adds a new station trait job: The Human AI.
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
2024-03-09 23:48:39 +01:00

333 lines
15 KiB
Plaintext

/obj/item/laser_pointer
name = "laser pointer"
desc = "Don't shine it in your eyes!"
icon = 'icons/obj/service/bureaucracy.dmi'
icon_state = "pointer"
inhand_icon_state = "pen"
worn_icon_state = "pen"
obj_flags = CONDUCTS_ELECTRICITY
item_flags = NOBLUDGEON
slot_flags = ITEM_SLOT_BELT
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 5, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 5)
w_class = WEIGHT_CLASS_SMALL
///Currently stored blulespace crystal, if any. Required to use the pointer through walls
var/obj/item/stack/ore/bluespace_crystal/crystal_lens
///Currently stored micro-laser diode
var/obj/item/stock_parts/micro_laser/diode
///Chance that the pointer dot will trigger a reaction from a mob/object
var/effectchance = 30
///Currently available battery charge of the laser pointer
var/energy = 10
///Maximum possible battery charge of the laser. Draining the battery puts the pointer in a recharge state, preventing use, which ends upon full recharge
var/max_energy = 10
///Maximum use range
var/max_range = 7
///Icon for the laser, affects both the laser dot and the laser pointer itself, as it shines a laser on the item itself
var/pointer_icon_state = null
///Whether the pointer is currently in a full recharge state. Triggered upon fully draining the battery
var/recharge_locked = FALSE
///Whether the pointer is currently recharging or not
var/recharging = FALSE
/obj/item/laser_pointer/red
pointer_icon_state = "red_laser"
/obj/item/laser_pointer/green
pointer_icon_state = "green_laser"
/obj/item/laser_pointer/blue
pointer_icon_state = "blue_laser"
/obj/item/laser_pointer/purple
pointer_icon_state = "purple_laser"
/obj/item/laser_pointer/Initialize(mapload)
. = ..()
diode = new(src)
if(!pointer_icon_state)
pointer_icon_state = pick("red_laser", "green_laser", "blue_laser", "purple_laser")
/obj/item/laser_pointer/Destroy(force)
QDEL_NULL(crystal_lens)
QDEL_NULL(diode)
return ..()
/obj/item/laser_pointer/Exited(atom/movable/gone, direction)
. = ..()
if(gone == crystal_lens)
crystal_lens = null
if(gone == diode)
diode = null
/obj/item/laser_pointer/upgraded/Initialize(mapload)
. = ..()
diode = new /obj/item/stock_parts/micro_laser/ultra
/obj/item/laser_pointer/infinite_range
name = "infinite laser pointer"
desc = "Used to shine in the eyes of Cyborgs who need a bit of a push, this works through camera consoles."
max_range = INFINITE
/obj/item/laser_pointer/infinite_range/Initialize(mapload)
. = ..()
diode = new /obj/item/stock_parts/servo/femto
/obj/item/laser_pointer/screwdriver_act(mob/living/user, obj/item/tool)
if(diode)
tool.play_tool_sound(src)
balloon_alert(user, "removed diode")
diode.forceMove(drop_location())
diode = null
return TRUE
/obj/item/laser_pointer/item_interaction(mob/living/user, obj/item/tool, list/modifiers, is_right_clicking)
. = ..()
if(. & ITEM_INTERACT_ANY_BLOCKER)
return .
if(isnull(crystal_lens))
return .
if(tool_behaviour != TOOL_WIRECUTTER && tool_behaviour != TOOL_HEMOSTAT)
return .
tool.play_tool_sound(src)
balloon_alert(user, "removed crystal lens")
crystal_lens.forceMove(drop_location())
crystal_lens = null
return ITEM_INTERACT_SUCCESS
/obj/item/laser_pointer/attackby(obj/item/attack_item, mob/user, params)
if(istype(attack_item, /obj/item/stock_parts/micro_laser))
if(diode)
balloon_alert(user, "already has a diode!")
return
var/obj/item/stock_parts/attack_diode = attack_item
if(crystal_lens && attack_diode.rating < 3) //only tier 3 and up are small enough to fit
to_chat(user, span_warning("You try to jam \the [attack_item.name] in place, but \the [crystal_lens.name] is in the way!"))
playsound(src, 'sound/machines/airlock_alien_prying.ogg', 20)
if(do_after(user, 2 SECONDS, src))
var/atom/atom_to_teleport = pick(user, attack_item)
if(atom_to_teleport == user)
to_chat(user, span_warning("You jam \the [attack_item.name] in too hard and break \the [crystal_lens.name] inside, teleporting you away!"))
user.drop_all_held_items()
else if(atom_to_teleport == attack_item)
attack_item.forceMove(drop_location())
to_chat(user, span_warning("You jam \the [attack_item.name] in too hard and break \the [crystal_lens.name] inside, teleporting \the [attack_item.name] away!"))
do_teleport(atom_to_teleport, get_turf(src), crystal_lens.blink_range, asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE)
qdel(crystal_lens)
return
if(!user.transferItemToLoc(attack_item, src))
return
playsound(src, 'sound/items/screwdriver.ogg', 30)
diode = attack_item
balloon_alert(user, "installed \the [diode.name]")
//we have a diode now, try starting a charge sequence in case the pointer was charging when we took out the diode
recharging = TRUE
START_PROCESSING(SSobj, src)
return TRUE
if(istype(attack_item, /obj/item/stack/ore/bluespace_crystal))
if(crystal_lens)
balloon_alert(user, "already has a lens!")
return
//the crystal stack we're trying to install a crystal from
var/obj/item/stack/ore/bluespace_crystal/crystal_stack = attack_item
if(diode && diode.rating < 3) //only lasers of tier 3 and up can house a lens
to_chat(user, span_warning("You try to jam \the [crystal_stack.name] in front of the diode, but it's a bad fit!"))
playsound(src, 'sound/machines/airlock_alien_prying.ogg', 20)
if(do_after(user, 2 SECONDS, src))
var/atom/atom_to_teleport = pick(user, src)
if(atom_to_teleport == user)
to_chat(user, span_warning("You press on \the [crystal_stack.name] too hard and are teleported away!"))
user.drop_all_held_items()
else if(atom_to_teleport == src)
forceMove(drop_location())
to_chat(user, span_warning("You press on \the [crystal_stack.name] too hard and \the [src] is teleported away!"))
do_teleport(atom_to_teleport, get_turf(src), crystal_stack.blink_range, asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE)
crystal_stack.use_tool(src, user, amount = 1) //use only one if we were installing from a stack of crystals
return
//the single crystal that we actually install
var/obj/item/stack/ore/bluespace_crystal/single_crystal = crystal_stack.split_stack(null, 1)
if(isnull(single_crystal))
return
if(!user.transferItemToLoc(single_crystal, src))
return
crystal_lens = single_crystal
playsound(src, 'sound/items/screwdriver2.ogg', 30)
balloon_alert(user, "installed \the [crystal_lens.name]")
to_chat(user, span_notice("You install a [crystal_lens.name] in [src]. \
It can now be used to shine through obstacles at the cost of double the energy drain."))
return TRUE
return ..()
/obj/item/laser_pointer/examine(mob/user)
. = ..()
if(in_range(user, src) || isobserver(user))
if(isnull(diode))
. += span_notice("The diode is missing.")
else
. += span_notice("A class <b>[diode.rating]</b> laser diode is installed. It is <i>screwed</i> in place.")
. += span_notice("A small display reads out that[recharge_locked ? " it is currently recharging to full, and" : ""] there is <b>[energy * 10]%</b> total charge remaining.")
if(crystal_lens)
. += span_notice("There is a <b>[crystal_lens.name]</b> fit neatly before the focus lens. It can be <i>plucked out</i> with some <i>wirecutters</i>.")
else if(diode) //hint at the ability to modify the pointer with a crystal only if we have a diode
. += span_notice("<i>You could examine it more thoroughly...</i>")
/obj/item/laser_pointer/examine_more(mob/user)
. = ..()
if(!isnull(crystal_lens) || isnull(diode))
return
switch(diode.rating)
if(1)
. += "<i>\The [diode.name] is fit neatly into the casing.</i>"
if(2)
. += "<i>\The [diode.name] is secured in place, with a little bit of room left between it and the focus lens.</i>"
if(3 to 4)
. += "<i>\The [diode.name]'s size is much smaller compared to the previous generation lasers, \
and the wide margin between it and the focus lens could probably house <b>a crystal</b> of some sort.</i>"
/obj/item/laser_pointer/afterattack(atom/target, mob/living/user, flag, params)
. = ..()
. |= AFTERATTACK_PROCESSED_ITEM
laser_act(target, user, params)
///Handles shining the clicked atom,
/obj/item/laser_pointer/proc/laser_act(atom/target, mob/living/user, params)
if(isnull(diode))
to_chat(user, span_notice("You point [src] at [target], but nothing happens!"))
return
if(!ISADVANCEDTOOLUSER(user))
to_chat(user, span_warning("You don't have the dexterity to do this!"))
return
if(HAS_TRAIT(user, TRAIT_CHUNKYFINGERS))
to_chat(user, span_warning("Your fingers can't press the button!"))
return
if(max_range != INFINITE)
if(!IN_GIVEN_RANGE(target, user, max_range))
to_chat(user, span_warning("\The [target] is too far away!"))
return
if(!(user in (view(max_range, target)))) //check if we are visible from the target's PoV
if(isnull(crystal_lens))
to_chat(user, span_warning("You can't point with [src] through walls!"))
return
if(!((user.sight & SEE_OBJS) || (user.sight & SEE_MOBS))) //only let it work if we have xray or thermals. mesons don't count because they are easier to get.
to_chat(user, span_notice("You can't quite make out your target and you fail to shine at it."))
return
add_fingerprint(user)
//nothing happens if the battery has been drained and has not fully recharged yet
if(recharge_locked)
to_chat(user, span_notice("You point [src] at [target], but it's still charging."))
return
//The message we send to the user upon using the pointer
var/outmsg
//The turf of the target we clicked on
var/turf/targloc = get_turf(target)
//human/alien mobs: if we aim for the eyes, chance to flash the target
if(iscarbon(target))
var/mob/living/carbon/target_humanoid = target
if(target_humanoid.stat == DEAD)
outmsg = span_notice("You point [src] at [target_humanoid], but [target_humanoid.p_they()] appear[target_humanoid.p_s()] to be dead!")
else if(user.zone_selected == BODY_ZONE_PRECISE_EYES)
//Intensity of the laser dot to pass to flash_act
var/severity = pick(0, 1, 2)
//chance to actually hit the eyes depends on internal component
if(prob(effectchance * diode.rating) && target_humanoid.flash_act(severity))
outmsg = span_notice("You blind [target_humanoid] by shining [src] in [target_humanoid.p_their()] eyes.")
log_combat(user, target_humanoid, "blinded with a laser pointer", src)
else
outmsg = span_warning("You fail to blind [target_humanoid] by shining [src] at [target_humanoid.p_their()] eyes!")
log_combat(user, target_humanoid, "attempted to blind with a laser pointer", src)
//borgs: chance to flash and paralyse the target
else if(iscyborg(target))
var/mob/living/silicon/target_sillycone = target
//chance to actually hit the eyes depends on internal component
if(target_sillycone.stat == DEAD)
outmsg = span_notice("You point [src] at [target_sillycone], but [target_sillycone.p_they()] appear[target_sillycone.p_s()] to be non-functioning.")
if(prob(effectchance * diode.rating) && target_sillycone.flash_act(affect_silicon = TRUE))
target_sillycone.set_temp_blindness_if_lower(5 SECONDS)
to_chat(target_sillycone, span_danger("Your sensors were overloaded by a laser!"))
outmsg = span_notice("You overload [target_sillycone] by shining [src] at [target_sillycone.p_their()] sensors.")
log_combat(user, target_sillycone, "shone in the sensors", src)
else
outmsg = span_warning("You fail to overload [target_sillycone] by shining [src] at [target_sillycone.p_their()] sensors!")
log_combat(user, target_sillycone, "attempted to shine in the sensors", src)
//cameras: chance to EMP the camera
else if(istype(target, /obj/machinery/camera))
var/obj/machinery/camera/target_camera = target
if(!target_camera.status && !target_camera.emped)
outmsg = span_notice("You point [src] at [target_camera], but it seems to be disabled.")
else if(prob(effectchance * diode.rating))
target_camera.emp_act(EMP_HEAVY)
outmsg = span_notice("You hit the lens of [target_camera] with [src], temporarily disabling the camera!")
log_combat(user, target_camera, "EMPed", src)
else
outmsg = span_warning("You miss the lens of [target_camera] with [src]!")
//catpeople: make any felinid near the target to face the target, chance for felinids to pounce at the light, stepping to the target
for(var/mob/living/carbon/human/target_felinid in view(1, targloc))
if(!isfelinid(target_felinid) || target_felinid.stat == DEAD || target_felinid.is_blind() || target_felinid.incapacitated())
continue
if(target_felinid.body_position == STANDING_UP)
target_felinid.setDir(get_dir(target_felinid, targloc)) // kitty always looks at the light
if(prob(effectchance * diode.rating))
target_felinid.visible_message(span_warning("[target_felinid] makes a grab for the light!"), span_userdanger("LIGHT!"))
target_felinid.Move(targloc)
log_combat(user, target_felinid, "moved with a laser pointer", src)
else
target_felinid.visible_message(span_notice("[target_felinid] looks briefly distracted by the light."), span_warning("You're briefly tempted by the shiny light..."))
else
target_felinid.visible_message(span_notice("[target_felinid] stares at the light."), span_warning("You stare at the light..."))
//The pointer is shining, change its sprite to show
icon_state = "pointer_[pointer_icon_state]"
//setup pointer blip
var/mutable_appearance/laser = mutable_appearance('icons/obj/weapons/guns/projectiles.dmi', pointer_icon_state)
var/list/modifiers = params2list(params)
if(modifiers)
if(LAZYACCESS(modifiers, ICON_X))
laser.pixel_x = (text2num(LAZYACCESS(modifiers, ICON_X)) - 16)
if(LAZYACCESS(modifiers, ICON_Y))
laser.pixel_y = (text2num(LAZYACCESS(modifiers, ICON_Y)) - 16)
else
laser.pixel_x = target.pixel_x + rand(-5,5)
laser.pixel_y = target.pixel_y + rand(-5,5)
if(outmsg)
to_chat(user, outmsg)
else
to_chat(user, span_info("You point [src] at [target]."))
//we have successfully shone our pointer, reduce our battery depending on whether we have an extra lens or not
energy -= crystal_lens ? 2 : 1
if(energy <= max_energy) //normal recharge, does not stop us from using the pointer
if(!recharging)
recharging = TRUE
START_PROCESSING(SSobj, src)
if(energy <= 0) //battery is completely dry, recharge the pointer to full then let us use it again
to_chat(user, span_warning("[src]'s battery is overused, it needs time to recharge!"))
recharge_locked = TRUE
//flash a pointer blip at the target
target.flick_overlay_view(laser, 1 SECONDS)
//reset pointer sprite
icon_state = "pointer"
/obj/item/laser_pointer/process(seconds_per_tick)
if(isnull(diode))
recharging = FALSE
return PROCESS_KILL
if(SPT_PROB(10 + diode.rating * 10, seconds_per_tick)) //+10% chance per diode tier to recharge one use per process
energy += 1
if(energy >= max_energy)
energy = max_energy
recharging = FALSE
recharge_locked = FALSE
return ..()