diff --git a/code/game/atoms.dm b/code/game/atoms.dm index ff9b3941cf5..d2e89e0ac15 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1769,3 +1769,25 @@ /atom/proc/InitializeAIController() if(ai_controller) ai_controller = new ai_controller(src) + +/** + * Point at an atom + * + * Intended to enable and standardise the pointing animation for all atoms + * + * Not intended as a replacement for the mob verb + */ +/atom/movable/proc/point_at(atom/A) + if(!isturf(loc)) + return FALSE + + var/turf/tile = get_turf(A) + if (!tile) + return FALSE + + var/turf/our_tile = get_turf(src) + var/obj/visual = new /obj/effect/temp_visual/point(our_tile, invisibility) + + animate(visual, pixel_x = (tile.x - our_tile.x) * world.icon_size + A.pixel_x, pixel_y = (tile.y - our_tile.y) * world.icon_size + A.pixel_y, time = 1.7, easing = EASE_OUT) + + return TRUE diff --git a/code/game/objects/items/wayfinding.dm b/code/game/objects/items/wayfinding.dm index 6fdfcda0deb..ea30535113e 100644 --- a/code/game/objects/items/wayfinding.dm +++ b/code/game/objects/items/wayfinding.dm @@ -1,121 +1,246 @@ +#define COOLDOWN_SPAWN 3 MINUTES +#define COOLDOWN_INTERACT 6 SECONDS +#define COOLDOWN_SLOGAN 5 MINUTES +#define COOLDOWN_SPEW 5 MINUTES /obj/machinery/pinpointer_dispenser name = "wayfinding pinpointer synthesizer" icon = 'icons/obj/machines/wayfinding.dmi' icon_state = "pinpointersynth" - desc = "Having trouble finding your way? This machine synthesizes pinpointers that point to common locations." + desc = "A machine given the thankless job of trying to sell wayfinding pinpointers. They point to common locations." density = FALSE layer = HIGH_OBJ_LAYER + armor = list(MELEE = 80, BULLET = 30, LASER = 30, ENERGY = 60, BOMB = 90, BIO = 0, RAD = 0, FIRE = 100, ACID = 80) + payment_department = ACCOUNT_CIV + light_power = 0.5 + light_range = MINIMUM_USEFUL_LIGHT_RANGE + ///List of user-specific cooldowns to prevent pinpointer spam. var/list/user_spawn_cooldowns = list() + ///List of user-specific cooldowns to prevent message spam. var/list/user_interact_cooldowns = list() - var/spawn_cooldown = 5 MINUTES //time per person to spawn another pinpointer - var/interact_cooldown = 20 SECONDS //time per person for subsequent interactions - var/start_bal = 200 //how much money it starts with to cover wayfinder refunds - var/refund_amt = 40 //how much money recycling a pinpointer rewards you + ///How many credits the dispenser account starts with to cover wayfinder refunds. + var/start_bal = 400 + ///How many credits recycling a pinpointer rewards you. + var/refund_amt = 40 var/datum/bank_account/synth_acc = new /datum/bank_account/remote - var/ppt_cost = 65 //Jan 6 '20: Assistant can buy one roundstart (125 cr starting) + var/ppt_cost = 0 //Jan 9 '21: 2560 had its difficulties for NT as well var/expression_timer + ///Avoid being Reddit. + var/funnyprob = 2 + ///List of slogans used by the dispenser to attract customers. + var/list/slogan_list = list("Find a wayfinding pinpointer? Give it to me! I'll make it worth your while. Please. Daddy needs his medicine.", //last sentence is a reference to Sealab 2021 + "See a wayfinding pinpointer? Don't let it go to the crusher! Recycle it with me instead. I'll pay you!", //I see these things heading for disposals through cargo all the time + "Need the disk? Can't get a pinpointer? Buy a wayfinding pinpointer and find the captain's office today!", + "Bleeding to death? Can't read? Find your way to medbay today!", //there are signs that point to medbay but you need basic literacy to get the most out of them + "Voted tenth best pinpointer in the universe in 2560!", //there were no more than ten pinpointers in the game in 2020 + "Helping assistants find the departments they tide since 2560.", //not really but it's advertising + "These pinpointers are flying out the airlock!", //because they're being thrown into space + "Grey pinpointers for the grey tide!", //I didn't pick the colour but it works + "Feeling lost? Find direction.", + "Automate your sense of direction. Buy a wayfinding pinpointer today!", + "Feed me a stray pinpointer.", //American Psycho reference + "We need a slogan!") //Liberal Crime Squad reference + ///Number of the list entry of the slogan we're up to. + var/slogan_entry = 0 + ///Next tick we can say a slogan. + COOLDOWN_DECLARE(next_slogan_tick) + ///Next tick the dispenser's spew rejection of non-wayfinding pinpointers can be triggered. + COOLDOWN_DECLARE(next_spew_tick) /obj/machinery/pinpointer_dispenser/Initialize(mapload) . = ..() - var/datum/bank_account/civ_acc = SSeconomy.get_dep_account(ACCOUNT_CIV) + var/datum/bank_account/civ_acc = SSeconomy.get_dep_account(payment_department) if(civ_acc) synth_acc.transfer_money(civ_acc, start_bal) //float has to come from somewhere, right? synth_acc.account_holder = name - desc += " Only [ppt_cost] credits! It also likes making costumes..." + desc += " [ppt_cost ? "Only [ppt_cost] credits! " : ""]It also synthesises costumes for some reason." - set_expression("neutral") + power_change() + + COOLDOWN_START(src, next_slogan_tick, COOLDOWN_SLOGAN) + slogan_list = shuffle(slogan_list) //minimise repetition + +/obj/machinery/pinpointer_dispenser/power_change() + . = ..() + cut_overlays() + if(powered()) + set_expression("veryhappy", 2 SECONDS) //v happy to be back in the pinpointer business + START_PROCESSING(SSmachines, src) + +/obj/machinery/pinpointer_dispenser/update_icon_state() + if(machine_stat & BROKEN) + set_light(0) + else if(powered()) + set_light(1.4) + else + set_light(0) + +/obj/machinery/pinpointer_dispenser/process(delta_time) + if(machine_stat & (BROKEN|NOPOWER)) + return PROCESS_KILL + + if(!length(slogan_list) || !COOLDOWN_FINISHED(src, next_slogan_tick)) + return + if(++slogan_entry > length(slogan_list)) + slogan_entry = 1 + var/slogan = slogan_list[slogan_entry] + say(slogan) + COOLDOWN_START(src, next_slogan_tick, COOLDOWN_SLOGAN) + +/obj/machinery/pinpointer_dispenser/Destroy() + for(var/i in 1 to rand(3, 9)) //Doesn't synthesise them in real time and instead stockpiles completed ones (though this is not how the cooldown works) + new /obj/item/pinpointer/wayfinding (loc) + say("Ouch.") + //An inexplicable explosion is never not funny plus it kind of explains why the machine just disappears + if(!isnull(loc)) + explosion(get_turf(src), devastation_range = 0, heavy_impact_range = 0, light_impact_range = 1, flash_range = 3, flame_range = 1, smoke = TRUE) + return ..() /obj/machinery/pinpointer_dispenser/attack_hand(mob/living/user) - if(world.time < user_interact_cooldowns[user.real_name]) - to_chat(user, "It doesn't respond.") + . = ..() + + if(machine_stat & (BROKEN|NOPOWER)) return - user_interact_cooldowns[user.real_name] = world.time + interact_cooldown + if(world.time < user_interact_cooldowns[user.real_name]) + set_expression("veryhappy", 2 SECONDS) + to_chat(user, "It just grins at you. Maybe you should give it a bit?") //telling instead of showing but I'm lazy + return + + user_interact_cooldowns[user.real_name] = world.time + COOLDOWN_INTERACT for(var/obj/item/pinpointer/wayfinding/WP in user.GetAllContents()) - set_expression("unsure", 2 SECONDS) - say("I can detect the pinpointer on you, [user.first_name()].") - user_spawn_cooldowns[user.real_name] = world.time + spawn_cooldown //spawn timer resets for trickers + set_expression("veryhappy", 2 SECONDS) + say("You already have a pinpointer!") return var/msg var/dispense = TRUE - var/obj/item/pinpointer/wayfinding/pointat - for(var/obj/item/pinpointer/wayfinding/WP in range(7, user)) - if(WP.Adjacent(user)) - set_expression("facepalm", 2 SECONDS) - say("[WP.owner == user.real_name ? "Your" : "A"] pinpointer is right there.") - pointat(WP) - user_spawn_cooldowns[user.real_name] = world.time + spawn_cooldown - return - else if(WP in oview(7, user)) - pointat = WP - break + var/pnpts_found = 0 + for(var/obj/item/pinpointer/wayfinding/WP in view(9, src)) + point_at(WP) + pnpts_found++ + + if(pnpts_found) + set_expression("veryhappy", 2 SECONDS) + say("[pnpts_found == 1 ? "There's a pinpointer" : "There are pinpointers"] there!") + return if(world.time < user_spawn_cooldowns[user.real_name]) var/secsleft = (user_spawn_cooldowns[user.real_name] - world.time) / 10 - msg += "to wait another [secsleft/60 > 1 ? "[round(secsleft/60,1)] minute\s" : "[round(secsleft)] second\s"]" + msg += "to wait [secsleft/60 > 1 ? "[round(secsleft/60,1)] more minute\s" : "[round(secsleft)] more second\s"] before I can give you another pinpointer" dispense = FALSE var/datum/bank_account/cust_acc = user.get_bank_account() - if(cust_acc) - if(!cust_acc.has_money(ppt_cost)) + if(ppt_cost) + if(!cust_acc) + msg += "a bank account to buy a pinpointer" + dispense = FALSE + else if(!cust_acc.has_money(ppt_cost)) msg += "[!msg ? "to find [ppt_cost-cust_acc.account_balance] more credit\s" : " and find [ppt_cost-cust_acc.account_balance] more credit\s"]" dispense = FALSE + else if(synth_acc.transfer_money(cust_acc, ppt_cost)) + dispense = TRUE if(!dispense) set_expression("sad", 2 SECONDS) - if(pointat) - msg += ". I suggest you get [pointat.owner == user.real_name ? "your" : "that"] pinpointer over there instead" - pointat(pointat) - say("You will need [msg], [user.first_name()].") - return - - if(synth_acc.transfer_money(cust_acc, ppt_cost)) + say("Sorry, [user.first_name()]! You'll need [msg]!") + else set_expression("veryhappy", 2 SECONDS) - say("That is [ppt_cost] credits. Here is your pinpointer.") + say("Here's your pinpointer!") var/obj/item/pinpointer/wayfinding/P = new /obj/item/pinpointer/wayfinding(get_turf(src)) - user_spawn_cooldowns[user.real_name] = world.time + spawn_cooldown + user_spawn_cooldowns[user.real_name] = world.time + COOLDOWN_SPAWN user.put_in_hands(P) P.owner = user.real_name /obj/machinery/pinpointer_dispenser/attackby(obj/item/I, mob/user, params) + if(machine_stat & (BROKEN|NOPOWER)) + return ..() + if(istype(I, /obj/item/pinpointer/wayfinding)) var/obj/item/pinpointer/wayfinding/WP = I + to_chat(user, "You put \the [WP] in the return slot.") - var/rfnd_amt - if((!WP.roundstart || WP.owner != user.real_name) && synth_acc.has_money(TRUE)) //can't recycle own pinpointer for money if not bought; given by a neutral quirk - if(synth_acc.has_money(refund_amt)) - rfnd_amt = refund_amt - else - rfnd_amt = synth_acc.account_balance - synth_acc._adjust_money(-rfnd_amt) - var/obj/item/holochip/HC = new /obj/item/holochip(user.loc) - HC.credits = rfnd_amt - HC.name = "[HC.credits] credit holochip" - if(istype(user, /mob/living/carbon/human)) - var/mob/living/carbon/human/H = user - H.put_in_hands(HC) - else - var/crap = pick(subtypesof(/obj/effect/spawner/bundle/costume)) //harmless garbage some people may appreciate - new crap(user.loc) - qdel(WP) - set_expression("happy", 2 SECONDS) - say("Thank you for recycling, [user.first_name()]! Here is [rfnd_amt ? "[rfnd_amt] credits." : "a freshly synthesized costume!"]") + + var/refundiscredits = FALSE + var/itsmypinpointer = TRUE + + //Will they meet the conditions to get a credit reward for recycling? + if(WP.owner != user.real_name) + itsmypinpointer = FALSE + + if(synth_acc.has_money(refund_amt) && !WP.roundstart) //can it afford to refund and is the pinpointer not from the quirk + refundiscredits = TRUE + qdel(WP) + synth_acc._adjust_money(-refund_amt) + var/obj/item/holochip/holochip = new (loc) + holochip.credits = refund_amt + holochip.name = "[holochip.credits] credit holochip" + if(ishuman(user)) + var/mob/living/carbon/human/customer = user + customer.put_in_hands(holochip) + + if(!refundiscredits) + qdel(WP) + var/costume = pick(subtypesof(/obj/effect/spawner/bundle/costume)) + new costume(user.loc) + + set_expression("veryhappy", 2 SECONDS) + + var/is_a_thing = "are [refund_amt] credit\s." + if(!refundiscredits) + is_a_thing = "is a freshly synthesised costume!" + if(prob(funnyprob)) + is_a_thing = "is a pulse rifle! Just kidding it's a costume." + + var/recycling = "recycling" + if(prob(funnyprob)) + recycling = "feeding me" + + //To imply they got a costume instead of money because it was their pinpointer they recycled + var/the_pinpointer = "your pinpointer" + if(!itsmypinpointer) + the_pinpointer = "that pinpointer" + + say("Thank you for [recycling] [the_pinpointer]! Here [is_a_thing]") + + return + + else if(istype(I, /obj/item/pinpointer)) + set_expression("sad", 2 SECONDS) + user_interact_cooldowns[user.real_name] = world.time + COOLDOWN_INTERACT + + //Any other type of pinpointer can make it throw up. + if(COOLDOWN_FINISHED(src, next_spew_tick)) + I.forceMove(loc) + visible_message("\The [src] smartly rejects [I].") + say("BLEURRRRGH!") + I.throw_at(user, 2, 3) + COOLDOWN_START(src, next_spew_tick, COOLDOWN_SPEW) + + return + + else if(I.force) + set_expression("sad", 2 SECONDS) + + return ..() /obj/machinery/pinpointer_dispenser/proc/set_expression(type, duration) cut_overlays() + + if(machine_stat & (BROKEN|NOPOWER)) + return + deltimer(expression_timer) add_overlay(type) if(duration) - expression_timer = addtimer(CALLBACK(src, .proc/set_expression, "neutral"), duration, TIMER_STOPPABLE) + expression_timer = addtimer(CALLBACK(src, .proc/set_expression, "happy"), duration, TIMER_STOPPABLE) -/obj/machinery/pinpointer_dispenser/proc/pointat(atom) - visible_message("[src] points at [atom].") - new /obj/effect/temp_visual/point(atom,invisibility) +/obj/machinery/pinpointer_dispenser/point_at(A) + . = ..() + visible_message("[src] points at [A]. [prob(funnyprob) ? "How'd it do that?" : ""]") //Pinpointer itself /obj/item/pinpointer/wayfinding //Help players new to a station find their way around diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 2e95c58d5d4..b6f5fe5fe00 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -571,20 +571,12 @@ set name = "Point To" set category = "Object" - if(!src || !isturf(src.loc)) - return FALSE if(client && !(A in view(client.view, src))) return FALSE if(istype(A, /obj/effect/temp_visual/point)) return FALSE - var/turf/tile = get_turf(A) - if (!tile) - return FALSE - - var/turf/our_tile = get_turf(src) - var/obj/visual = new /obj/effect/temp_visual/point(our_tile, invisibility) - animate(visual, pixel_x = (tile.x - our_tile.x) * world.icon_size + A.pixel_x, pixel_y = (tile.y - our_tile.y) * world.icon_size + A.pixel_y, time = 1.7, easing = EASE_OUT) + point_at(A) return TRUE