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"