diff --git a/code/datums/outfits/jobs/science.dm b/code/datums/outfits/jobs/science.dm
index 6b4b8a99cb..07782df22d 100644
--- a/code/datums/outfits/jobs/science.dm
+++ b/code/datums/outfits/jobs/science.dm
@@ -30,6 +30,13 @@
id_type = /obj/item/card/id/science
suit = /obj/item/clothing/suit/storage/toggle/labcoat/science
+/decl/hierarchy/outfit/job/science/xenofauna
+ name = OUTFIT_JOB_NAME("Xenofauna Technician")
+ uniform = /obj/item/clothing/under/xenofauna
+ id_type = /obj/item/card/id/science
+ suit = /obj/item/clothing/suit/storage/toggle/labcoat/science
+ l_ear = /obj/item/radio/headset/explorer
+
/decl/hierarchy/outfit/job/science/roboticist
name = OUTFIT_JOB_NAME("Roboticist")
uniform = /obj/item/clothing/under/rank/roboticist
@@ -40,4 +47,4 @@
pda_type = /obj/item/pda/roboticist
backpack = /obj/item/storage/backpack
satchel_one = /obj/item/storage/backpack/satchel/norm
- suit = /obj/item/clothing/suit/storage/toggle/labcoat/roboticist
\ No newline at end of file
+ suit = /obj/item/clothing/suit/storage/toggle/labcoat/roboticist
diff --git a/code/game/jobs/job/science.dm b/code/game/jobs/job/science.dm
index a66173f47e..9e0a20b94a 100644
--- a/code/game/jobs/job/science.dm
+++ b/code/game/jobs/job/science.dm
@@ -127,6 +127,7 @@
title = "Xenofauna Technician"
title_blurb = "A Xenofauna Technician works with the alien fauna living in the territory around the station, heading out in the field to tag and assess the \
local animal populations. They are also responsible for training and tending to the station's small drake population."
+ title_outfit = /decl/hierarchy/outfit/job/science/xenofauna
//////////////////////////////////
// Roboticist
diff --git a/code/game/objects/items/devices/gps.dm b/code/game/objects/items/devices/gps.dm
index 941a2199e4..8ac1978ecd 100644
--- a/code/game/objects/items/devices/gps.dm
+++ b/code/game/objects/items/devices/gps.dm
@@ -10,6 +10,9 @@ var/global/list/GPS_list = list()
origin_tech = list(TECH_MATERIAL = 2, TECH_BLUESPACE = 2, TECH_MAGNET = 1)
matter = list(MAT_STEEL = 500)
+ var/tag_category // Any special category for this tracker to sit in (used by xenofauna tags)
+ var/list/tag_categories // Any special categories this tracker should show (used in xenofauna GPS)
+
var/gps_tag = "GEN0"
var/emped = FALSE
var/tracking = FALSE // Will not show other signals or emit its own signal if false.
@@ -26,21 +29,25 @@ var/global/list/GPS_list = list()
/obj/item/gps/Initialize()
. = ..()
- compass = new(src)
+ create_compass()
GPS_list += src
- name = "global positioning system ([gps_tag])"
+ name = "[initial(name)] ([gps_tag])"
update_holder()
update_icon()
+/obj/item/gps/proc/create_compass()
+ compass = new(src)
+
/obj/item/gps/proc/check_visible_to_holder()
- . = (holder && (holder.get_active_hand() == src || holder.get_inactive_hand() == src))
+ . = istype(compass) && holder && (holder.get_active_hand() == src || holder.get_inactive_hand() == src)
/obj/item/gps/proc/update_holder()
if(holder && loc != holder)
GLOB.moved_event.unregister(holder, src)
GLOB.dir_set_event.unregister(holder, src)
- holder.client?.screen -= compass
+ if(compass)
+ holder.client?.screen -= compass
holder = null
if(istype(loc, /mob))
@@ -55,12 +62,12 @@ var/global/list/GPS_list = list()
if(holder.client)
if(check_visible_to_holder())
holder.client.screen |= compass
- else
+ else if(compass)
holder.client.screen -= compass
else
STOP_PROCESSING(SSobj, src)
is_in_processing_list = FALSE
- if(holder?.client)
+ if(holder?.client && compass)
holder.client.screen -= compass
/obj/item/gps/pickup()
@@ -96,7 +103,7 @@ var/global/list/GPS_list = list()
QDEL_NULL(compass)
/obj/item/gps/proc/can_track(var/obj/item/gps/other, var/reachable_z_levels)
- if(!other.tracking || other.emped || other.hide_signal)
+ if(!other.tracking || other.emped || other.hide_signal || (other.tag_category && !(other.tag_category in tag_categories)))
return FALSE
var/turf/origin = get_turf(src)
var/turf/target = get_turf(other)
@@ -110,7 +117,8 @@ var/global/list/GPS_list = list()
return (target.z in reachable_z_levels)
/obj/item/gps/proc/update_compass(var/update_compass_icon)
- compass.hide_waypoints(FALSE)
+ if(compass)
+ compass.hide_waypoints(FALSE)
var/turf/my_turf = get_turf(src)
for(var/thing in tracking_devices)
var/obj/item/gps/gps = locate(thing)
@@ -120,13 +128,15 @@ var/global/list/GPS_list = list()
continue
var/turf/gps_turf = get_turf(gps)
var/gps_tag = LAZYACCESS(showing_tracked_names, thing) ? gps.gps_tag : null
- if(istype(gps_turf))
- compass.set_waypoint("\ref[gps]", gps_tag, gps_turf.x, gps_turf.y, gps_turf.z, LAZYACCESS(tracking_devices, "\ref[gps]"))
- else
- compass.set_waypoint("\ref[gps]", gps_tag, 0, 0, 0, LAZYACCESS(tracking_devices, "\ref[gps]"))
- if(can_track(gps) && gps_turf && my_turf && gps_turf.z == my_turf.z)
- compass.show_waypoint("\ref[gps]")
- compass.rebuild_overlay_lists(update_compass_icon)
+ if(compass)
+ if(istype(gps_turf))
+ compass.set_waypoint("\ref[gps]", gps_tag, gps_turf.x, gps_turf.y, gps_turf.z, LAZYACCESS(tracking_devices, "\ref[gps]"))
+ else
+ compass.set_waypoint("\ref[gps]", gps_tag, 0, 0, 0, LAZYACCESS(tracking_devices, "\ref[gps]"))
+ if(can_track(gps) && gps_turf && my_turf && gps_turf.z == my_turf.z)
+ compass.show_waypoint("\ref[gps]")
+ if(compass)
+ compass.rebuild_overlay_lists(update_compass_icon)
/obj/item/gps/AltClick(mob/user)
toggletracking(user)
@@ -137,7 +147,7 @@ var/global/list/GPS_list = list()
if(emped)
to_chat(user, "It's busted!")
return
-
+
toggle_tracking()
if(tracking)
to_chat(user, "[src] is no longer tracking, or visible to other GPS devices.")
@@ -194,7 +204,7 @@ var/global/list/GPS_list = list()
dat["curr_z"] = curr.z
dat["curr_z_name"] = strip_improper(using_map.get_zlevel_name(curr.z))
dat["z_level_detection"] = using_map.get_map_levels(curr.z, long_range)
-
+
var/list/gps_list = list()
for(var/obj/item/gps/G in GPS_list - src)
@@ -250,7 +260,7 @@ var/global/list/GPS_list = list()
dat += "
"
var/gps_ref = "\ref[gps["ref"]]"
dat += "| [gps["gps_tag"]] | [gps["area_name"]] | "
-
+
if(istype(gps_data["ref"], /obj/item/gps/internal/poi))
dat += "[gps["local"] ? "[gps["direction"]] Dist: [round(gps["distance"], 10)]m" : "[gps["z_name"]]"] | "
else
@@ -294,7 +304,8 @@ var/global/list/GPS_list = list()
if(href_list["stop_track"])
var/gps_ref = href_list["stop_track"]
var/obj/item/gps/gps = locate(gps_ref)
- compass.clear_waypoint(gps_ref)
+ if(compass)
+ compass.clear_waypoint(gps_ref)
LAZYREMOVE(tracking_devices, gps_ref)
LAZYREMOVE(showing_tracked_names, gps_ref)
if(istype(gps) && !QDELETED(gps))
@@ -347,7 +358,7 @@ var/global/list/GPS_list = list()
/obj/item/gps/proc/SetTag(new_tag)
gps_tag = uppertext(copytext(sanitize(new_tag), 1, 11))
- name = "global positioning system ([gps_tag])"
+ name = "[initial(name)] ([gps_tag])"
/obj/item/gps/on // Defaults to off to avoid polluting the signal list with a bunch of GPSes without owners. If you need to spawn active ones, use these.
@@ -392,6 +403,11 @@ var/global/list/GPS_list = list()
icon_state = "gps-sci"
gps_tag = "SCI0"
+/obj/item/gps/xenofauna
+ icon_state = "gps-sci"
+ gps_tag = "XEN0"
+ tag_categories = list("XENOFAUNA")
+
/obj/item/gps/science/on
tracking = TRUE
diff --git a/code/modules/catalogue/atoms.dm b/code/modules/catalogue/atoms.dm
index f085d61058..5e6fd7abd4 100644
--- a/code/modules/catalogue/atoms.dm
+++ b/code/modules/catalogue/atoms.dm
@@ -16,18 +16,18 @@
if(!LAZYLEN(data))
to_chat(user, span("warning", "\The [src] is not interesting enough to catalogue."))
return FALSE
- else
- // Check if this has nothing new on it.
- var/has_new_data = FALSE
- for(var/t in data)
- var/datum/category_item/catalogue/item = GLOB.catalogue_data.resolve_item(t)
- if(!item.visible)
- has_new_data = TRUE
- break
- if(!has_new_data)
- to_chat(user, span("warning", "Scanning \the [src] would provide no new information."))
- return FALSE
+ // Check if this has nothing new on it.
+ var/has_new_data = FALSE
+ for(var/t in data)
+ var/datum/category_item/catalogue/item = GLOB.catalogue_data.resolve_item(t)
+ if(!item.visible)
+ has_new_data = TRUE
+ break
+
+ if(!has_new_data)
+ to_chat(user, span("warning", "Scanning \the [src] would provide no new information."))
+ return FALSE
return TRUE
@@ -53,29 +53,25 @@
return catalogue_data
/mob/living/carbon/human/get_catalogue_data()
- var/list/data = list()
// First, handle robot-ness.
var/beep_boop = get_FBP_type()
switch(beep_boop)
if(FBP_CYBORG)
- data += /datum/category_item/catalogue/technology/cyborgs
+ LAZYADD(., /datum/category_item/catalogue/technology/cyborgs)
if(FBP_POSI)
- data += /datum/category_item/catalogue/technology/positronics
+ LAZYADD(., /datum/category_item/catalogue/technology/positronics)
if(FBP_DRONE)
- data += /datum/category_item/catalogue/technology/drone/drones
+ LAZYADD(., /datum/category_item/catalogue/technology/drone/drones)
// Now for species.
- if(!(beep_boop in list(FBP_POSI, FBP_DRONE))) // Don't give the species entry if they are a posi or drone.
- if(species && LAZYLEN(species.catalogue_data))
- data += species.catalogue_data
- return data
+ // Don't give the species entry if they are a posi or drone.
+ if(!(beep_boop in list(FBP_POSI, FBP_DRONE)) && species && LAZYLEN(species.catalogue_data))
+ LAZYADD(., species.catalogue_data)
/mob/living/silicon/robot/get_catalogue_data()
- var/list/data = list()
switch(braintype)
if(BORG_BRAINTYPE_CYBORG)
- data += /datum/category_item/catalogue/technology/cyborgs
+ LAZYADD(., /datum/category_item/catalogue/technology/cyborgs)
if(BORG_BRAINTYPE_POSI)
- data += /datum/category_item/catalogue/technology/positronics
+ LAZYADD(., /datum/category_item/catalogue/technology/positronics)
if(BORG_BRAINTYPE_DRONE)
- data += /datum/category_item/catalogue/technology/drone/drones
- return data
\ No newline at end of file
+ LAZYADD(., /datum/category_item/catalogue/technology/drone/drones)
diff --git a/code/modules/clothing/head/hood.dm b/code/modules/clothing/head/hood.dm
index 61533fbbc2..3b01369e9b 100644
--- a/code/modules/clothing/head/hood.dm
+++ b/code/modules/clothing/head/hood.dm
@@ -193,6 +193,13 @@
siemens_coefficient = 0.9
armor = list(melee = 30, bullet = 20, laser = 20, energy = 20, bomb = 35, bio = 75, rad = 35)
+/obj/item/clothing/head/hood/explorer/xenofauna
+ name = "xenofauna field hood"
+ desc = "A lightly armoured hood for surveying harsh environments."
+ icon_state = "xenofauna"
+ siemens_coefficient = 0.5
+ armor = list(melee = 20, bullet = 10, laser = 10, energy = 10, bomb = 25, bio = 90, rad = 60)
+
// Costumes
/obj/item/clothing/head/hood/carp_hood
name = "carp hood"
diff --git a/code/modules/clothing/suits/hooded.dm b/code/modules/clothing/suits/hooded.dm
index 89b0093365..a716ccdf99 100644
--- a/code/modules/clothing/suits/hooded.dm
+++ b/code/modules/clothing/suits/hooded.dm
@@ -425,5 +425,16 @@
/obj/item/material/knife,
/obj/item/tank,
/obj/item/radio,
- /obj/item/pickaxe
- )
+ /obj/item/pickaxe,
+ /obj/item/cataloguer,
+ /obj/item/specimen_tagger
+ )
+
+/obj/item/clothing/suit/storage/hooded/explorer/xenofauna
+ name = "xenofauna field suit"
+ desc = "A lightly armoured suit for surveying harsh environments."
+ icon_state = "xenofauna"
+ item_state = "xenofauna"
+ siemens_coefficient = 0.5
+ armor = list(melee = 20, bullet = 10, laser = 10, energy = 10, bomb = 25, bio = 90, rad = 60) // Better bio/rad protection than explo, but less armour.
+ hoodtype = /obj/item/clothing/head/hood/explorer/xenofauna
diff --git a/code/modules/clothing/under/miscellaneous.dm b/code/modules/clothing/under/miscellaneous.dm
index fa629bd2ee..ba5b462d20 100644
--- a/code/modules/clothing/under/miscellaneous.dm
+++ b/code/modules/clothing/under/miscellaneous.dm
@@ -1210,12 +1210,17 @@ Uniforms and such
worn_state = "bathrobe"
/obj/item/clothing/under/explorer
- desc = "A green uniform for operating in hazardous environments."
+ desc = "A grey and cyan uniform for working in the field."
name = "explorer's jumpsuit"
icon_state = "explorer"
+/obj/item/clothing/under/xenofauna
+ desc = "A grey and purple uniform for working in the field."
+ name = "xenofauna technician's jumpsuit"
+ icon_state = "xenofauna"
+
/obj/item/clothing/under/explorer/armored
- desc = "A green uniform for operating in hazardous environments. This one looks like it's been modified."
+ desc = "A grey and green uniform for operating in hazardous environments. This one looks like it's been modified."
armor = list(melee = 10, bullet = 10, laser = 10,energy = 10, bomb = 10, bio = 10, rad = 30)
armorsoak = list(melee = 5, bullet = 5, laser = 5,energy = 5, bomb = 5, bio = 5, rad = 0)
diff --git a/code/modules/persistence/datum/persistence_datum.dm b/code/modules/persistence/datum/persistence_datum.dm
index 9ff1598c7c..c33ec59188 100644
--- a/code/modules/persistence/datum/persistence_datum.dm
+++ b/code/modules/persistence/datum/persistence_datum.dm
@@ -9,6 +9,8 @@
var/entries_decay_at // Set in rounds. This controls when item messages start getting scrambled.
var/entry_decay_weight = 0.5
var/has_admin_data
+ var/area_restricted = TRUE
+ var/station_restricted = TRUE
/datum/persistent/New()
SetFilename()
@@ -71,14 +73,15 @@
/datum/persistent/proc/IsValidEntry(var/atom/entry)
if(!istype(entry))
return FALSE
- if(GetEntryAge(entry) >= entries_expire_at)
+ if(!isnull(entries_expire_at) && GetEntryAge(entry) >= entries_expire_at)
return FALSE
var/turf/T = get_turf(entry)
- if(!T || !(T.z in using_map.station_levels) )
- return FALSE
- var/area/A = get_area(T)
- if(!A || (A.area_flags & AREA_FLAG_IS_NOT_PERSISTENT))
+ if(station_restricted && (!T || !(T.z in using_map.station_levels) ))
return FALSE
+ if(area_restricted)
+ var/area/A = get_area(T)
+ if(!A || (A.area_flags & AREA_FLAG_IS_NOT_PERSISTENT))
+ return FALSE
return TRUE
/datum/persistent/proc/GetEntryAge(var/atom/entry)
diff --git a/code/modules/persistence/effects/graffiti.dm b/code/modules/persistence/effects/graffiti.dm
index 369f9156b4..43596057b5 100644
--- a/code/modules/persistence/effects/graffiti.dm
+++ b/code/modules/persistence/effects/graffiti.dm
@@ -43,4 +43,4 @@
if(can_modify)
. = "[save_graffiti.message] | [save_graffiti.author] | Destroy | "
else
- . = "[save_graffiti.message] | [save_graffiti.author] | "
\ No newline at end of file
+ . = "[save_graffiti.message] | [save_graffiti.author] | "
diff --git a/code/modules/persistence/items/specimen.dm b/code/modules/persistence/items/specimen.dm
new file mode 100644
index 0000000000..41d1069709
--- /dev/null
+++ b/code/modules/persistence/items/specimen.dm
@@ -0,0 +1,44 @@
+// Currently unticked pending full implementation of the required data structures
+// and tracking/load logic. Needs to account for DMMS load, critters spawning in POIs,
+// critters spawning on station, etc. Fair bit of design work to consider.
+/datum/persistent/specimen
+ name = "specimen"
+ entries_expire_at = 10
+ area_restricted = FALSE
+ station_restricted = FALSE
+
+/datum/persistent/specimen/CreateEntryInstance(var/turf/creating, var/list/token)
+ new /obj/item/gps/specimen_tag(
+ creating,
+ token["age"]+1,
+ token["implanted_by"],
+ token["specimen_id"],
+ token["specimen_gender"],
+ token["physical_info"],
+ token["behavior_info"],
+ text2path(token["specimen_type"])
+ )
+
+/datum/persistent/specimen/GetEntryAge(var/atom/entry)
+ var/obj/item/gps/specimen_tag/save_specimen = entry
+ if(!istype(save_specimen))
+ return 0
+ return save_specimen.age
+
+/datum/persistent/specimen/IsValidEntry(atom/entry)
+ . = ..()
+ var/obj/item/gps/specimen_tag/save_specimen = entry
+ return istype(save_specimen) && save_specimen.has_been_implanted() && save_specimen.implanted_in.stat != DEAD
+
+/datum/persistent/specimen/CompileEntry(var/atom/entry, var/write_file)
+ . = ..()
+ var/obj/item/gps/specimen_tag/save_specimen = entry
+ if(!istype(save_specimen))
+ return
+ save_specimen.update_from_animal()
+ LAZYADDASSOC(., "implanted_by", save_specimen.implanted_by)
+ LAZYADDASSOC(., "specimen_id", save_specimen.gps_tag)
+ LAZYADDASSOC(., "specimen_type", save_specimen.implanted_in.type)
+ LAZYADDASSOC(., "specimen_gender", save_specimen.gender)
+ LAZYADDASSOC(., "physical_info", save_specimen.physical_info)
+ LAZYADDASSOC(., "behavior_info", save_specimen.behavioral_info)
diff --git a/code/modules/persistence/specimens.dm b/code/modules/persistence/specimens.dm
new file mode 100644
index 0000000000..819602df8c
--- /dev/null
+++ b/code/modules/persistence/specimens.dm
@@ -0,0 +1,292 @@
+// Device used to implant, remove or read specimen tags.
+
+/* Notes on specimen tagger and expected flow:
+ * - Xenofauna player uses tagger (/obj/item/specimen_tagger) on appropriate critter (appropriate type, has cataloguer info).
+ * - Tag (/obj/item/gps/specimen_tag) is created and registered in the critter.
+ * - Xenofauna players can then track the tag via GPS to scan, remove, etc.
+ * - When persistent specimens are committed, tags will be loaded and assigned to mobs at world init.
+ */
+
+/obj/item/specimen_tagger
+ name = "specimen tagger"
+ desc = "A handheld device used to implant, remove and read xenofauna tracking tags from local specimens. Not for use on crewmembers."
+ icon = 'icons/obj/specimen_tagger.dmi'
+ icon_state = "tagger"
+ force = 0
+ item_flags = NOBLUDGEON
+ var/tag_id = "FAUNA0"
+ var/static/list/unsuitable_tag_types = list(
+ // Do not specimen tag your crewmates
+ /mob/living/carbon,
+ /mob/living/simple_mob/animal/sif/grafadreka/trained,
+ // Do not specimen tag pre-mapped critters
+ /mob/living/simple_mob/animal/passive/opossum/poppy,
+ /mob/living/simple_mob/animal/passive/dog/corgi/Ian,
+ /mob/living/simple_mob/animal/passive/cat/runtime
+ )
+
+/obj/item/specimen_tagger/attack_self(mob/user)
+ var/new_tag = input("Please enter desired tag.", name, tag_id) as text
+ if(QDELETED(src) || QDELETED(user) || user.incapacitated() || loc != user)
+ return TRUE
+ new_tag = uppertext(copytext(sanitize(new_tag), 1, 11))
+ if(!length(new_tag))
+ return TRUE
+ tag_id = new_tag
+ to_chat(usr, "You set the tracker tag to '[tag_id]'.")
+ return TRUE
+
+/obj/item/specimen_tagger/examine(mob/user)
+ . = ..()
+ . += "Use this on a living animal on help intent to read an existing tracker, grab intent to tag an animal with a tracker, and any other intent to remove an existing tracker."
+
+/obj/item/specimen_tagger/attack(mob/living/M, mob/living/user, target_zone, attack_modifier)
+ switch(user.a_intent)
+ if(I_HELP)
+ try_read_tag(user, M)
+ if(I_GRAB)
+ try_implant_tag(user, M)
+ else
+ try_remove_tag(user, M)
+ return TRUE
+
+/obj/item/specimen_tagger/proc/try_read_tag(var/mob/user, var/mob/living/target)
+ var/obj/item/gps/specimen_tag/xenotag = locate() in target
+ if(!istype(xenotag) || !xenotag.has_been_implanted())
+ to_chat(user, SPAN_WARNING("\The [target] has not been tagged."))
+ return FALSE
+ to_chat(user, "Specimen data for [xenotag.gps_tag]:")
+ to_chat(user, "Species: [target.real_name]")
+ to_chat(user, "Tag duration: [xenotag.age] shift\s")
+ to_chat(user, "Tagged by: [xenotag.implanted_by]")
+ to_chat(user, "Physical notes: [xenotag.physical_info]")
+ to_chat(user, "Behavioral notes: [xenotag.behavioral_info]")
+ return TRUE
+
+/obj/item/specimen_tagger/proc/check_can_tag(var/mob/user, var/mob/living/target)
+ if(QDELETED(target) || !istype(target) || target.stat == DEAD || target.isSynthetic())
+ to_chat(user, SPAN_WARNING("Xenofauna specimens need to be living organic creatures."))
+ return FALSE
+ if(!LAZYLEN(target.get_catalogue_data()))
+ to_chat(user, SPAN_WARNING("There's no scientific reason to tag \the [target]."))
+ return FALSE
+ if(target.key || target.client || is_type_in_list(target, unsuitable_tag_types))
+ to_chat(user, SPAN_WARNING("\The [target] is not suitable for tagging."))
+ return FALSE
+ var/obj/item/gps/specimen_tag/xenotag = locate(/obj/item/gps/specimen_tag) in target
+ if(istype(xenotag) && xenotag.has_been_implanted())
+ to_chat(user, SPAN_WARNING("\The [target] has already been tagged."))
+ return FALSE
+ return TRUE
+
+/obj/item/specimen_tagger/proc/try_implant_tag(var/mob/user, var/mob/living/target)
+ if(!check_can_tag(user, target))
+ return FALSE
+ user.visible_message(SPAN_NOTICE("\The [user] begins tagging \the [target] with \the [src]..."))
+ if(!do_after(user, 3 SECONDS, target) || !check_can_tag(user, target))
+ return FALSE
+ var/obj/item/gps/specimen_tag/xenotag = new
+ xenotag.SetTag(tag_id)
+ xenotag.implanted_by = user.real_name
+ if(user.mind)
+ var/user_title = user.mind.assigned_role || user.mind.role_alt_title
+ if(user_title)
+ xenotag.implanted_by = "[xenotag.implanted_by], [user_title]"
+
+ xenotag.implant(target)
+ user.visible_message(SPAN_NOTICE("\The [user] tags \the [target] with \a [xenotag]!"))
+ return TRUE
+
+/obj/item/specimen_tagger/proc/can_remove_tag(var/mob/user, var/mob/living/target)
+ if(!istype(target))
+ to_chat(user, SPAN_WARNING("\The [target] is not a xenofauna specimen."))
+ return FALSE
+ var/obj/item/gps/specimen_tag/xenotag = locate() in target
+ if(!istype(xenotag) || !xenotag.has_been_implanted())
+ to_chat(user, SPAN_WARNING("\The [target] has not been tagged."))
+ return FALSE
+ return TRUE
+
+/obj/item/specimen_tagger/proc/try_remove_tag(var/mob/user, var/mob/living/target)
+ if(!can_remove_tag(user, target))
+ return FALSE
+ var/obj/item/gps/specimen_tag/xenotag = locate() in target
+ if(!istype(xenotag))
+ return FALSE
+ user.visible_message(SPAN_NOTICE("\The [user] starts removing \the [xenotag] from \the [target] with \the [src]..."))
+ if(!do_after(user, 3 SECONDS, target) || !can_remove_tag(user, target))
+ return FALSE
+ if(!istype(xenotag))
+ return FALSE
+ qdel(xenotag)
+ user.visible_message(SPAN_NOTICE("\The [user] removes \the [xenotag] from \the [target]!"))
+ return TRUE
+
+// Specimen tag itself.
+/obj/item/gps/specimen_tag
+ name = "xenofauna tracker"
+ gps_tag = "FAUNA0"
+ icon = 'icons/obj/specimen_tag.dmi'
+ icon_state = "gps-tag"
+ w_class = ITEMSIZE_TINY
+ tag_category = "XENOFAUNA"
+
+ var/age = 0
+ var/mob/living/implanted_in
+ var/implanted_by
+ var/physical_info = "No notes recorded."
+ var/behavioral_info = "No notes recorded."
+
+/obj/item/gps/specimen_tag/Initialize(mapload, _age, _implanted_by, _specimen_id, _specimen_gender, _physical_info, _behavioral_info, _specimen_type)
+ // If we have a specimen, set up our data.
+ if(_specimen_type)
+ var/mob/living/critter = new _specimen_type(get_turf(src))
+ implant(critter, TRUE)
+ if(_specimen_gender)
+ critter.gender = _specimen_gender
+ if(_age)
+ age = _age
+ if(_specimen_id)
+ gps_tag = _specimen_id
+ if(_physical_info)
+ physical_info = _physical_info
+ if(_behavioral_info)
+ behavioral_info = _behavioral_info
+ if(_implanted_by)
+ implanted_by = _implanted_by
+ . = ..()
+ if(!tracking)
+ toggletracking()
+
+/obj/item/gps/specimen_tag/Destroy()
+ clear_implanted()
+ // TODO: persistent tags
+ // SSpersistence.forget_value(src, /datum/persistent/specimen)
+ . = ..()
+
+/obj/item/gps/specimen_tag/Move()
+ . = ..()
+ if(implanted_in && loc != implanted_in)
+ clear_implanted()
+
+// Specimen tags are just for tracking, they don't work as held GPS.
+/obj/item/gps/specimen_tag/attack_hand(mob/living/user)
+ toggletracking(user)
+ return TRUE
+/obj/item/gps/specimen_tag/check_visible_to_holder()
+ return FALSE
+/obj/item/gps/specimen_tag/create_compass()
+ return
+/obj/item/gps/specimen_tag/display_list()
+ return list()
+
+/obj/item/gps/specimen_tag/proc/has_been_implanted()
+ return !QDELETED(implanted_in) && istype(implanted_in) && loc == implanted_in
+
+/obj/item/gps/specimen_tag/proc/implant(var/mob/target, var/implanted_in_init = FALSE)
+ forceMove(target)
+ implanted_in = target
+ GLOB.destroyed_event.register(implanted_in, src, /obj/item/gps/specimen_tag/proc/clear_implanted)
+ // TODO: persistent tags
+ // SSpersistence.track_value(src, /datum/persistent/specimen)
+ if(!implanted_in_init)
+ generate_critter_info()
+
+/obj/item/gps/specimen_tag/proc/generate_critter_info()
+
+ var/list/possible_physical_info
+ var/list/possible_behavioral_info
+ var/list/catalogue_data = implanted_in.get_catalogue_data()
+ for(var/catalogue_data_type in catalogue_data)
+ if(ispath(catalogue_data_type, /datum/category_item/catalogue/fauna))
+ var/datum/category_item/catalogue/fauna/fauna_data = GLOB.catalogue_data.resolve_item(catalogue_data_type)
+ var/notes = fauna_data.get_fauna_physical_notes()
+ if(!isnull(notes))
+ LAZYDISTINCTADD(possible_physical_info, notes)
+ notes = fauna_data.get_fauna_behavior_notes()
+ if(!isnull(notes))
+ LAZYDISTINCTADD(possible_behavioral_info, notes)
+
+ if(LAZYLEN(possible_physical_info))
+ physical_info = pick(possible_physical_info)
+ else
+ physical_info = "No notes recorded."
+
+ if(LAZYLEN(possible_behavioral_info))
+ behavioral_info = pick(possible_behavioral_info)
+ else
+ behavioral_info = "No notes recorded."
+
+/obj/item/gps/specimen_tag/proc/clear_implanted()
+ if(implanted_in)
+ GLOB.destroyed_event.unregister(implanted_in, src)
+ implanted_in = null
+
+/obj/item/gps/specimen_tag/proc/update_from_animal()
+ return
+
+// Mob helpers/overrides.
+/mob/living/examine(mob/user, infix, suffix)
+ . = ..()
+ var/obj/item/gps/specimen_tag/xenotag = locate() in src
+ if(istype(xenotag) && xenotag.has_been_implanted())
+ . += "\The [src] has been tagged with \a [xenotag]."
+
+// Lists of strings used by the specimen tracking system to add flavour to
+// specimens. Static lists in procs to allow overriding while also not putting
+// a massive string list on every single cataloguer fauna entry.
+/datum/category_item/catalogue/fauna/proc/get_fauna_physical_notes()
+ var/static/list/fauna_physical_notes = list(
+ "Perpetually smells like mold no matter what we do about it.",
+ "Perpetually smells like mildew no matter what we do about it.",
+ "Perpetually smells like sifsap no matter what we do about it.",
+ "Seems to always have an itch in the spot it can't reach.",
+ "Vocalizations are notably harsh and loud for the species.",
+ "Vocalizations are notably soft and quiet for the species.",
+ "Has a notch in its left ear.",
+ "Has a notch in its right ear.",
+ "Is missing its left ear.",
+ "Is missing its right ear.",
+ "Is missing its left eye.",
+ "Is missing its right eye.",
+ "Has had the very tip of its tail chewed off.",
+ "Might have trouble with its hearing.",
+ "Is more scar tissue than animal at this point.",
+ "Walks with a limp.",
+ "Has two differently colored eyes.",
+ "Might have a cold...",
+ "Is allergic to nuts.",
+ "Is allergic to berries.",
+ "Is allergic to dairy."
+ )
+ return fauna_physical_notes
+
+/datum/category_item/catalogue/fauna/proc/get_fauna_behavior_notes()
+ var/static/list/fauna_behavior_notes = list(
+ "Likes rolling around in the moss with reckless abandon.",
+ "Enjoys munching on frostbelles the most, as a treat.",
+ "Enjoys munching on wabback the most, as a treat.",
+ "Enjoys munching on eyebulbs the most, as a treat.",
+ "Constantly sniffs around at everything new.",
+ "Seems super friendly! Probably won't bite. Probably.",
+ "Seems rather stand-offish. Mind the personal bubble.",
+ "Loves a good back scritch.",
+ "Loves a good head scritch.",
+ "Loves a good behind the ear scritch.",
+ "Seems to hate the world and everything in it.",
+ "Just tolerates being pet, but certainly doesn't enjoy it.",
+ "Often sticks its head into snowbanks to contemplate the state of things.",
+ "Enjoys singing along to songs only it can hear. Mostly just sounds like an animal wailing.",
+ "Would rather be fishing.",
+ "Partakes in many siestas.",
+ "Struggles with object permanence.",
+ "Is very picky about food; maybe it's a texture thing.",
+ "Is a sentient garbage disposal for anything even remotely edible.",
+ "Loves swimming and splashing around in water.",
+ "Sinks like a rock the moment it enters water.",
+ "Gets lost easily.",
+ "Enjoys soft things. It'd have a bed of plushies if it knew what a bed was.",
+ "Never seems to be in a rush to go anywhere...",
+ "Has gotta go fast at all times."
+ )
+ return fauna_behavior_notes
diff --git a/code/modules/projectiles/guns/energy/phase.dm b/code/modules/projectiles/guns/energy/phase.dm
index 89cdbd23d3..db6eeed74a 100644
--- a/code/modules/projectiles/guns/energy/phase.dm
+++ b/code/modules/projectiles/guns/energy/phase.dm
@@ -14,9 +14,9 @@
/obj/item/gun/energy/phasegun/tranq_rifle
name = "tranquilizer rifle"
desc = "A niche RayZar product designed for nonlethal animal control. A specialized emitter disrupts the nervous system of the target, eventually inducing sleep. Only rated for use on wildlife."
- icon_state = "phaserifle"
- item_state = "phaserifle"
- wielded_item_state = "phaserifle-wielded"
+ icon_state = "tranqrifle"
+ item_state = "tranqrifle"
+ wielded_item_state = "tranqrifle-wielded"
w_class = ITEMSIZE_LARGE
slot_flags = SLOT_BACK
charge_cost = 140
@@ -27,7 +27,7 @@
/obj/item/gun/energy/phasegun/tranq_pistol
name = "tranquilizer pistol"
desc = "A niche RayZar product designed for nonlethal animal control. A specialized emitter disrupts the nervous system of the target, eventually inducing sleep. Only rated for use on wildlife."
- icon_state = "phase"
+ icon_state = "tranq"
item_state = "taser"
w_class = ITEMSIZE_NORMAL
slot_flags = SLOT_BELT|SLOT_HOLSTER
diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi
index 46c8ca868a..f458bd0902 100644
Binary files a/icons/mob/head.dmi and b/icons/mob/head.dmi differ
diff --git a/icons/mob/suit.dmi b/icons/mob/suit.dmi
index 686590cd2a..c17a2c1492 100644
Binary files a/icons/mob/suit.dmi and b/icons/mob/suit.dmi differ
diff --git a/icons/mob/uniform.dmi b/icons/mob/uniform.dmi
index d36a8da407..d46efdf7d1 100644
Binary files a/icons/mob/uniform.dmi and b/icons/mob/uniform.dmi differ
diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi
index 481a11f9c1..7dcd7e4926 100644
Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ
diff --git a/icons/obj/clothing/suits.dmi b/icons/obj/clothing/suits.dmi
index 69308450dd..889f440450 100644
Binary files a/icons/obj/clothing/suits.dmi and b/icons/obj/clothing/suits.dmi differ
diff --git a/icons/obj/clothing/uniforms.dmi b/icons/obj/clothing/uniforms.dmi
index e6e2322c60..745167c740 100644
Binary files a/icons/obj/clothing/uniforms.dmi and b/icons/obj/clothing/uniforms.dmi differ
diff --git a/icons/obj/gun_energy.dmi b/icons/obj/gun_energy.dmi
index fcd7bc33f4..e250dc57ba 100644
Binary files a/icons/obj/gun_energy.dmi and b/icons/obj/gun_energy.dmi differ
diff --git a/icons/obj/specimen_tag.dmi b/icons/obj/specimen_tag.dmi
new file mode 100644
index 0000000000..946cec22f9
Binary files /dev/null and b/icons/obj/specimen_tag.dmi differ
diff --git a/icons/obj/specimen_tagger.dmi b/icons/obj/specimen_tagger.dmi
new file mode 100644
index 0000000000..696d41919d
Binary files /dev/null and b/icons/obj/specimen_tagger.dmi differ
diff --git a/maps/cynosure/cynosure-2.dmm b/maps/cynosure/cynosure-2.dmm
index fc4c68a3e4..39f335f47c 100644
--- a/maps/cynosure/cynosure-2.dmm
+++ b/maps/cynosure/cynosure-2.dmm
@@ -15428,9 +15428,9 @@
icon_state = "0-4"
},
/obj/structure/table/rack,
-/obj/item/gps/explorer,
-/obj/item/gps/explorer,
-/obj/item/gps/explorer,
+/obj/item/gps/xenofauna,
+/obj/item/gps/xenofauna,
+/obj/item/gps/xenofauna,
/obj/item/melee/umbrella/random,
/obj/item/melee/umbrella/random,
/obj/item/melee/umbrella/random,
diff --git a/maps/cynosure/structures/closets/misc.dm b/maps/cynosure/structures/closets/misc.dm
index 36159bcf13..6fcae52d01 100644
--- a/maps/cynosure/structures/closets/misc.dm
+++ b/maps/cynosure/structures/closets/misc.dm
@@ -85,37 +85,25 @@
name = "xenofauna technician locker"
req_access = list(access_xenofauna)
starts_with = list(
- /obj/item/clothing/under/explorer,
- /obj/item/clothing/suit/armor/pcarrier/explorer/light,
- /obj/item/clothing/head/helmet/explorer,
- /obj/item/clothing/suit/storage/hooded/explorer,
+ /obj/item/clothing/under/xenofauna,
+ /obj/item/clothing/suit/storage/hooded/explorer/xenofauna,
/obj/item/clothing/mask/gas/explorer,
/obj/item/clothing/shoes/boots/winter/explorer,
/obj/item/clothing/gloves/black,
+ /obj/item/clothing/suit/storage/hooded/wintercoat/parka/purple,
/obj/item/radio/headset/explorer,
/obj/item/flashlight,
- /obj/item/gps/explorer,
- /obj/item/storage/box/flare,
+ /obj/item/gps/xenofauna,
/obj/item/geiger,
/obj/item/cell/device,
/obj/item/radio,
- /obj/item/stack/marker_beacon/thirty,
- /obj/item/cataloguer
- )
-
-/obj/structure/closet/secure_closet/xenofauna/Initialize()
- if(prob(50))
- starts_with += /obj/item/storage/backpack/rucksack
- else
- starts_with += /obj/item/storage/backpack/satchel/norm
- if(prob(75))
- starts_with += /obj/item/material/knife/tacknife/survival
- else
- starts_with += /obj/item/material/knife/machete
- return ..()
+ /obj/item/cataloguer,
+ /obj/item/storage/backpack/satchel/norm,
+ /obj/item/material/knife/tacknife/survival,
+ /obj/item/specimen_tagger
+ )
//SAR Lockers
-
/obj/structure/closet/secure_closet/sar
name = "search and rescue locker"
desc = "Supplies for a wilderness first responder."
diff --git a/polaris.dme b/polaris.dme
index 1aaf9a3629..0b676c2680 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -2791,6 +2791,7 @@
#include "code\modules\persistence\filth.dm"
#include "code\modules\persistence\graffiti.dm"
#include "code\modules\persistence\noticeboard.dm"
+#include "code\modules\persistence\specimens.dm"
#include "code\modules\persistence\datum\persistence_datum.dm"
#include "code\modules\persistence\effects\filth.dm"
#include "code\modules\persistence\effects\graffiti.dm"