diff --git a/code/game/objects/items/devices/laserpointer.dm b/code/game/objects/items/devices/laserpointer.dm index 37298fb8c37..f595d376c3b 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(!isfeline(H) || H.incapacitated() || H.is_blind()) // SKYRAT EDIT - FELINE TRAITS. Was: isfelinid(H) + //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(!isfeline(target_felinid) || target_felinid.stat == DEAD || target_felinid.is_blind() || target_felinid.incapacitated()) // SKYRAT EDIT - FELINE TRAITS. Was: isfelinid(H) 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