From b22ff1a4ebfd0a1dd1b75d6979edc73e6f4556b2 Mon Sep 17 00:00:00 2001 From: Sealed101 Date: Sat, 12 Aug 2023 22:41:12 +0300 Subject: [PATCH] Laser pointer update: Shining Through Walls Edition (feat. fixes!) (#77007) # _PR PSA_ ![augh](https://github.com/tgstation/tgstation/assets/75863639/6dc87fc7-65a3-4b7c-9b8d-a1432cacbe93) ## About The Pull Request Cleans up code for laser pointers, fixing some bugs like the forever-charging state or affecting dead cats along the way. Remaining charge is now available upon examine. Canonizes #45834 by implementing an upgrade to the laser pointers: installing a bluespace crystal into a laser with tier 3 or higher laser diode lets it shine through walls. Using an upgraded laser uses twice the charge of a normal one. Of course, you can only shine it on something if you can see the target behind the wall, like via x-ray or thermals. Mesons don't count, however. If one tries to jam a crystal into a pointer with a tier 1/2 laser (or a tier 1/2 laser in a pointer with an installed crystal), _something_ will get teleported, crushing the crystal. You can uninstall the crystal with wirecutters or a hemostat. The pointer will _hint_ on closer examination (`examine_more`) at a possibility of a crystal being installed if you upgrade the laser (different messages for tier 1/2/3,4). Removes one stupid 1% increase for a recharge chance per process tick if your laser was in a full recharge state because it was insignificant and irrelevant. i've had a branch for this for almost 9 months and i was always laying it off for some day later. today i just completely fucked the branch. whoops. i'm not even sure at this point what else did i fix while here, double whoops ## Why It's Good For The Game Closes #45834 - Canonizes a bug into a feature. Fixes #77003 - lol Cleaner code, possibly more robust even. Seeing the remaining charge was not available at all and the only hint was when you tried shining the pointer on something. That sucks. ## Changelog :cl: add: you can upgrade laser pointers with a bluespace crystal to let them shine through walls at double the power cost, if the laser in the pointer is of tier 3 or higher. qol: laser pointer charge can be seen by examining it fix: fixed laser pointers luring dead cats when shone upon code: laser pointer code cleaned up a tad /:cl: --------- Co-authored-by: Jacquerel --- .../objects/items/devices/laserpointer.dm | 288 +++++++++++++----- 1 file changed, 207 insertions(+), 81 deletions(-) diff --git a/code/game/objects/items/devices/laserpointer.dm b/code/game/objects/items/devices/laserpointer.dm index f56cc83e116..ccb7cf702c7 100644 --- a/code/game/objects/items/devices/laserpointer.dm +++ b/code/game/objects/items/devices/laserpointer.dm @@ -5,19 +5,29 @@ icon_state = "pointer" inhand_icon_state = "pen" worn_icon_state = "pen" - var/pointer_icon_state flags_1 = CONDUCT_1 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) + custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 5, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 5) w_class = WEIGHT_CLASS_SMALL - var/turf/pointer_loc - var/energy = 10 - var/max_energy = 10 + ///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 - var/recharging = FALSE + ///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 - var/obj/item/stock_parts/micro_laser/diode //used for upgrading! + ///Whether the pointer is currently recharging or not + var/recharging = FALSE /obj/item/laser_pointer/red pointer_icon_state = "red_laser" @@ -35,7 +45,19 @@ . = ..() diode = new(src) if(!pointer_icon_state) - pointer_icon_state = pick("red_laser","green_laser","blue_laser","purple_laser") + 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) . = ..() @@ -44,128 +66,229 @@ /obj/item/laser_pointer/screwdriver_act(mob/living/user, obj/item/tool) if(diode) tool.play_tool_sound(src) - to_chat(user, span_notice("You remove the [diode.name] from \the [src].")) + balloon_alert(user, "removed diode") diode.forceMove(drop_location()) diode = null return TRUE -/obj/item/laser_pointer/attackby(obj/item/W, mob/user, params) - if(istype(W, /obj/item/stock_parts/micro_laser)) - if(!diode) - if(!user.transferItemToLoc(W, src)) - return - diode = W - to_chat(user, span_notice("You install a [diode.name] in [src].")) - else - to_chat(user, span_warning("[src] already has a diode installed!")) - else - return ..() +/obj/item/laser_pointer/tool_act(mob/living/user, obj/item/tool) + . = ..() + if(isnull(crystal_lens) || !(tool.tool_behaviour == TOOL_WIRECUTTER || tool.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 TRUE + +/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(!diode) + if(isnull(diode)) . += span_notice("The diode is missing.") else . += span_notice("A class [diode.rating] laser diode is installed. It is screwed in place.") + . += span_notice("A small display reads out that[recharge_locked ? " it is currently recharging to full, and" : ""] there is [energy * 10]% total charge remaining.") + if(crystal_lens) + . += span_notice("There is a [crystal_lens.name] fit neatly before the focus lens. It can be plucked out with some wirecutters.") + else if(diode) //hint at the ability to modify the pointer with a crystal only if we have a diode + . += span_notice("You could examine it more thoroughly...") + +/obj/item/laser_pointer/examine_more(mob/user) + . = ..() + if(!isnull(crystal_lens) || isnull(diode)) + return + switch(diode.rating) + if(1) + . += "\The [diode.name] is fit neatly into the casing." + if(2) + . += "\The [diode.name] is secured in place, with a little bit of room left between it and the focus lens." + if(3 to 4) + . += "\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 a crystal of some sort." /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( !(user in (viewers(7,target))) ) - return - if (!diode) + if(isnull(diode)) to_chat(user, span_notice("You point [src] at [target], but nothing happens!")) return - if (!ISADVANCEDTOOLUSER(user)) + 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(!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 is drained + //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 + //human/alien mobs: if we aim for the eyes, chance to flash the target if(iscarbon(target)) - var/mob/living/carbon/C = target - if(user.zone_selected == BODY_ZONE_PRECISE_EYES) - - var/severity = 1 - if(prob(33)) - severity = 2 - else if(prob(50)) - severity = 0 + 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) && C.flash_act(severity)) - outmsg = span_notice("You blind [C] by shining [src] in [C.p_their()] eyes.") - log_combat(user, C, "blinded with a laser pointer",src) + 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 [C] by shining [src] at [C.p_their()] eyes!") - log_combat(user, C, "attempted to blind with a laser pointer",src) + 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) - //robots + //borgs: chance to flash and paralyse the target else if(iscyborg(target)) - var/mob/living/silicon/S = target - log_combat(user, S, "shone in the sensors", src) + var/mob/living/silicon/target_sillycone = target //chance to actually hit the eyes depends on internal component - if(prob(effectchance * diode.rating) && S.flash_act(affect_silicon = TRUE)) - S.set_temp_blindness_if_lower(5 SECONDS) - to_chat(S, span_danger("Your sensors were overloaded by a laser!")) - outmsg = span_notice("You overload [S] by shining [src] at [S.p_their()] sensors.") + 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 [S] by shining [src] at [S.p_their()] sensors!") + 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 + //cameras: chance to EMP the camera else if(istype(target, /obj/machinery/camera)) - var/obj/machinery/camera/C = target - if(prob(effectchance * diode.rating)) - C.emp_act(EMP_HEAVY) - outmsg = span_notice("You hit the lens of [C] with [src], temporarily disabling the camera!") - log_combat(user, C, "EMPed", src) + 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 [C] with [src]!") + outmsg = span_warning("You miss the lens of [target_camera] with [src]!") - //catpeople - for(var/mob/living/carbon/human/H in view(1,targloc)) - if(!isfelinid(H) || H.incapacitated() || H.is_blind()) + //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(user.body_position == STANDING_UP) - H.setDir(get_dir(H,targloc)) // kitty always looks at the light + 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)) - H.visible_message(span_warning("[H] makes a grab for the light!"),span_userdanger("LIGHT!")) - H.Move(targloc) - log_combat(user, H, "moved with a laser pointer",src) + 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 - H.visible_message(span_notice("[H] looks briefly distracted by the light."), span_warning("You're briefly tempted by the shiny light...")) + 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 - H.visible_message(span_notice("[H] stares at the light."), span_warning("You stare at the light...")) + target_felinid.visible_message(span_notice("[target_felinid] stares at the light."), span_warning("You stare at the light...")) - //cats! - for(var/mob/living/simple_animal/pet/cat/C in view(1,targloc)) + //cats! - chance for any cat near the target to pounce at the light, stepping to the target + for(var/mob/living/simple_animal/pet/cat/target_kitty in view(1, targloc)) + if(target_kitty.stat == DEAD) + continue if(prob(effectchance * diode.rating)) - if(C.resting) - C.set_resting(FALSE, instant = TRUE) - C.visible_message(span_notice("[C] pounces on the light!"),span_warning("LIGHT!")) - C.Move(targloc) - C.Immobilize(1 SECONDS) + if(target_kitty.resting) + target_kitty.set_resting(FALSE, instant = TRUE) + target_kitty.visible_message(span_notice("[target_kitty] pounces on the light!"), span_warning("LIGHT!")) + target_kitty.Move(targloc) + target_kitty.Immobilize(1 SECONDS) else - C.visible_message(span_notice("[C] looks uninterested in your games."),span_warning("You spot [user] shining [src] at you. How insulting!")) + target_kitty.visible_message(span_notice("[target_kitty] looks uninterested in your games."), span_warning("You spot [user] shining [src] at you. How insulting!")) - //laser pointer image + //The pointer is shining, change its sprite to show icon_state = "pointer_[pointer_icon_state]" - var/mutable_appearance/laser = mutable_appearance('icons/obj/weapons/guns/projectiles.dmi', pointer_icon_state, 10) + + //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)) @@ -181,23 +304,26 @@ else to_chat(user, span_info("You point [src] at [target].")) - energy -= 1 - if(energy <= max_energy) + //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) + 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 - targloc.flick_overlay_view(laser, 1 SECONDS) + //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(!diode) + if(isnull(diode)) recharging = FALSE return PROCESS_KILL - if(SPT_PROB(10 + diode.rating*10 - recharge_locked*1, seconds_per_tick)) //t1 is 20, 2 40 + 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