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