diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm
index c4528ed7ec8a..f9b5d2d64f46 100644
--- a/code/_globalvars/misc.dm
+++ b/code/_globalvars/misc.dm
@@ -132,3 +132,5 @@ GLOBAL_VAR(obfs_z)
/// List of giant lizards that are alive.
GLOBAL_LIST_EMPTY(giant_lizards_alive)
+/// List of F5CT Field Camera Tripods
+GLOBAL_LIST_EMPTY_TYPED(deployed_tripod_cameras, /obj/structure/overwatch_camera_tripod)
diff --git a/code/game/machinery/vending/vendor_types/squad_prep/squad_engineer.dm b/code/game/machinery/vending/vendor_types/squad_prep/squad_engineer.dm
index 50ecbef3b1a7..9d685e9a5647 100644
--- a/code/game/machinery/vending/vendor_types/squad_prep/squad_engineer.dm
+++ b/code/game/machinery/vending/vendor_types/squad_prep/squad_engineer.dm
@@ -83,6 +83,7 @@ GLOBAL_LIST_INIT(cm_vending_gear_engi, list(
list("Motion Detector", 8, /obj/item/device/motiondetector, null, VENDOR_ITEM_REGULAR),
list("Whistle", 3, /obj/item/clothing/accessory/device/whistle, null, VENDOR_ITEM_REGULAR),
list("Synthetic Reset Key", 10, /obj/item/device/defibrillator/synthetic, null, VENDOR_ITEM_REGULAR),
+ list("FCT - Field Camera Tripod", 5, /obj/item/device/overwatch_camera/tripod, null, VENDOR_ITEM_REGULAR),
list("BINOCULARS", 0, null, null, null),
list("Binoculars", 5, /obj/item/device/binoculars, null, VENDOR_ITEM_REGULAR),
diff --git a/code/modules/cm_marines/equipment/gear.dm b/code/modules/cm_marines/equipment/gear.dm
index 33fb84c2942d..ba4ea6f5e1b5 100644
--- a/code/modules/cm_marines/equipment/gear.dm
+++ b/code/modules/cm_marines/equipment/gear.dm
@@ -357,3 +357,220 @@
/obj/item/device/overwatch_camera/see_emote(mob/living/sourcemob, emote, audible)
SEND_SIGNAL(src, COMSIG_BROADCAST_SEE_EMOTE, sourcemob, emote, audible, loc == sourcemob && audible)
+
+/obj/item/device/overwatch_camera/tripod
+ name = "FTC Tripod Camera"
+ desc = "A Motoca-430-T deployable tripod camera that connects to the overwatch network. It can be renamed and deployed."
+ icon = 'icons/overwatch.dmi' // ToDO: Get real sprites
+ icon_state = "undeployed"
+ desc_lore = "Following modernisation efforts in the Marine'70 program, USCM Platoons were shrunk and squads re-organised to emphasise individual firepower and mobility. The Motoca-430-T, the precursor to the Motoca-500 Helmet Camera, was commissioned by the Department of Defense to be utilised by Colonial Marine squads in establishing secure perimeters and watching rear areas remotely through the Overwatch system."
+ var/label
+ var/datum/squad/squad
+
+/obj/item/device/overwatch_camera/tripod/Initialize(mapload, ...)
+ . = ..()
+ camera = new /obj/structure/machinery/camera/overwatch(src)
+ AddComponent(/datum/component/overwatch_console_control)
+
+/obj/item/device/overwatch_camera/tripod/Destroy()
+ QDEL_NULL(camera)
+ return ..()
+
+/obj/item/device/overwatch_camera/tripod/attack_self(mob/user)
+ ..()
+ var/choice = tgui_alert(user, "What would you like to do with [src]?", "Tripod Camera", list("Rename", "Deploy", "Cancel"))
+ switch(choice)
+ if("Cancel")
+ return
+ if("Rename")
+ var/new_name = tgui_input_text(user, "Enter new name for the camera:", "Rename Camera", label ? label : initial(name), MAX_NAME_LEN, ui_state=GLOB.not_incapacitated_state, encode=FALSE)
+ if(!new_name)
+ return
+ new_name = trim_right(replace_non_alphanumeric_plus(new_name))
+ if(!length(new_name))
+ to_chat(user, SPAN_WARNING("Invalid name."))
+ return
+ label = new_name
+ name = new_name
+ if(camera)
+ camera.c_tag = new_name
+ to_chat(user, SPAN_NOTICE("Camera renamed to [name]."))
+ return
+ if("Deploy")
+ deploy_tripod(user)
+
+/obj/item/device/overwatch_camera/tripod/proc/deploy_tripod(mob/user)
+ var/datum/squad/user_squad = null // find squad for addition to label
+
+ if(ishuman(user)) // synths can place so not strict check
+ var/mob/living/carbon/human/human_user = user
+ user_squad = human_user.assigned_squad
+ if(isyautja(user))
+ to_chat(user, SPAN_WARNING("You can't think of a reason to interact with [src] and decide to leave it alone."))
+ return
+ if(user.is_mob_incapacitated())
+ return
+ // if(user. != src)
+ // to_chat(user, SPAN_WARNING("You need to hold [src] in your hand to deploy it!"))
+ // return
+
+ var/turf/deploy_turf = get_turf(user)
+ if(!deploy_turf)
+ return
+
+ var/area/deploy_area = get_area(deploy_turf)
+ if(!deploy_area.allow_construction)
+ to_chat(user, SPAN_WARNING("You cannot deploy [src] here!"))
+ return
+ if(istype(deploy_area, /area/shuttle))
+ to_chat(user, SPAN_WARNING("You cannot deploy [src] in a shuttle area.")) // i copied this from M2C so idk if this is necessary?
+ return
+ if(!istype(deploy_turf, /turf/open))
+ to_chat(user, SPAN_WARNING("[src] must be placed on a solid surface!"))
+ return
+
+ for(var/obj/blocking_object in deploy_turf)
+ if(blocking_object.density && blocking_object != src)
+ to_chat(user, SPAN_WARNING("[blocking_object] is blocking the deployment spot!"))
+ return
+
+ if(!do_after(user, 3 SECONDS, INTERRUPT_ALL, BUSY_ICON_BUILD))
+ to_chat(user, SPAN_WARNING("You must stand still while deploying the tripod."))
+ return
+
+ if(user.stat != CONCIOUS || user.is_mob_incapacitated()) //not sure if this is the same check or not :D
+ return
+
+ if(user.get_active_hand() != src)
+ to_chat(user, SPAN_WARNING("You must hold [src] in your hand to deploy it!"))
+ return
+
+ var/base_label = label ? label : initial(name)
+ var/final_label = user_squad ? "[user_squad.name] - [base_label]" : base_label
+
+ var/obj/structure/overwatch_camera_tripod/deployed_structure = new(deploy_turf) // transform to new struc
+ deployed_structure.label = final_label
+ deployed_structure.name = final_label
+ deployed_structure.squad = user_squad
+ deployed_structure.icon_state = "deployed"
+
+ if(camera)
+ camera.forceMove(deployed_structure)
+ camera.c_tag = final_label
+ camera.status = TRUE
+ deployed_structure.camera = camera
+ src.camera = null
+
+ to_chat(user, SPAN_NOTICE("You deploy [src]."))
+ user.temp_drop_inv_item()
+ qdel(src)
+
+/obj/structure/overwatch_camera_tripod
+ name = "FTC Tripod Camera"
+ desc = "A Motoca-430-T deployed tripod camera connected to the overwatch network."
+ icon = 'icons/overwatch.dmi' // ToDO: Get real sprites
+ icon_state = "deployed"
+ density = TRUE
+ anchored = TRUE
+ layer = OBJ_LAYER
+ desc_lore = "Following modernisation efforts in the Marine'70 program, USCM Platoons were shrunk and squads re-organised to emphasise individual firepower and mobility. The Motoca-430-T, the precursor to the Motoca-500 Helmet Camera, was commissioned by the Department of Defense to be utilised by Colonial Marine squads in establishing secure perimeters and watching rear areas remotely through the Overwatch system."
+ var/label = "Tripod Camera"
+ var/obj/structure/machinery/camera/camera
+ var/datum/squad/squad
+ var/slash_count = 0 // tracks xeno slashes 4 breaking
+
+/obj/structure/overwatch_camera_tripod/Initialize(mapload)
+ . = ..()
+ icon_state = "deployed"
+ camera = new /obj/structure/machinery/camera/overwatch(src)
+ camera.c_tag = label
+ camera.status = TRUE
+ AddComponent(/datum/component/overwatch_console_control)
+ GLOB.deployed_tripod_cameras += src
+
+/obj/structure/overwatch_camera_tripod/Destroy()
+ GLOB.deployed_tripod_cameras -= src
+ QDEL_NULL(camera)
+ return ..()
+
+/obj/structure/overwatch_camera_tripod/examine(mob/user)
+ . = ..()
+ to_chat(user, SPAN_INFO("The label label reads: [label]")) // ToDO: This maybe should be in the description box I just don't know how to add it atm
+ if(squad)
+ to_chat(user, SPAN_INFO("It is currently assigned to squad: [squad.name]")) // ToDO: This maybe should be in the description box I just don't know how to add it atm
+
+/obj/structure/overwatch_camera_tripod/attack_hand(mob/user)
+ if(user.a_intent != INTENT_HELP) // I've left this in just in case maints want me to change the tgui menu to intent handling or smth.
+ return ..()
+ var/choice = tgui_alert(user, "What would you like to do with [src]?", "Tripod Camera", list("Rename", "Pick Up", "Cancel"))
+ switch(choice)
+ if("Cancel")
+ return
+ if("Rename")
+ if(isyautja(user))
+ to_chat(user, SPAN_WARNING("You can't think of a reason to interact with [src] and decide to leave it alone."))
+ return
+ var/new_name = tgui_input_text(user, "Enter new label for the camera:", "Rename Camera", label, MAX_NAME_LEN, ui_state=GLOB.not_incapacitated_state, encode=FALSE)
+ if(!new_name)
+ return
+ new_name = trim_right(replace_non_alphanumeric_plus(new_name))
+ if(!length(new_name))
+ to_chat(user, SPAN_WARNING("Invalid name."))
+ return
+ label = new_name
+ name = new_name
+ if(camera)
+ camera.c_tag = new_name
+ to_chat(user, SPAN_NOTICE("[src] renamed to [name]."))
+ return
+ if("Pick Up")
+ if(isyautja(user))
+ to_chat(user, SPAN_WARNING("You can't think of a reason to interact with [src] and decide to leave it alone."))
+ return
+ if(!user.Adjacent(src))
+ to_chat(user, SPAN_WARNING("You must be closer to pick up [src]."))
+ return
+ if(!do_after(user, 2 SECONDS, INTERRUPT_ALL, BUSY_ICON_GENERIC))
+ to_chat(user, SPAN_WARNING("You were interrupted while picking up the [src]."))
+ return
+ // Create a new tripod item from the structure
+ undeploy(user)
+ return // not sure if i need this here
+
+/obj/structure/overwatch_camera_tripod/attack_alien(mob/living/carbon/xenomorph/Xeno)
+ if(islarva(Xeno))
+ return
+ slash_count++
+ Xeno.animation_attack_on(src)
+ Xeno.flick_attack_overlay(src, "slash")
+ playsound(loc, 'sound/weapons/slash.ogg', 25, 1)
+ if(slash_count >= 3)
+ Xeno.visible_message(SPAN_DANGER("[Xeno] slashes [src] apart!"),
+ SPAN_DANGER("You tear through [src]!"))
+ undeploy()
+ else
+ Xeno.visible_message(SPAN_DANGER("[Xeno] slashes [src]!"),
+ SPAN_DANGER("You slash [src]!"))
+ return XENO_ATTACK_ACTION
+
+/obj/structure/overwatch_camera_tripod/proc/undeploy(mob/user)
+ var/obj/item/device/overwatch_camera/tripod/new_tripod = new(get_turf(src))
+ new_tripod.label = label
+ new_tripod.name = label
+ new_tripod.squad = squad
+ if(camera)
+ camera.forceMove(new_tripod)
+ camera.c_tag = label
+ camera.status = TRUE
+ new_tripod.camera = camera
+ src.camera = null
+ if(user && ishuman(user))
+ user.put_in_hands(new_tripod)
+ to_chat(user, SPAN_NOTICE("You disassemble [src]."))
+ else
+ new_tripod.visible_message(SPAN_WARNING("[new_tripod] falls to the floor."))
+ qdel(src)
+
+/obj/structure/overwatch_camera_tripod/ex_act(severity)
+ if(severity >= EXPLOSION_THRESHOLD_LOW) // no idea if i need to add this or it's inherited from parent somewhere
+ undeploy()
diff --git a/code/modules/cm_marines/overwatch.dm b/code/modules/cm_marines/overwatch.dm
index 01d277572ee7..413a49e0abee 100644
--- a/code/modules/cm_marines/overwatch.dm
+++ b/code/modules/cm_marines/overwatch.dm
@@ -360,6 +360,40 @@ GLOBAL_LIST_EMPTY_TYPED(active_overwatch_consoles, /obj/structure/machinery/comp
leader_count++
marine_count--
+ for(var/obj/structure/overwatch_camera_tripod/tripod_camera in GLOB.deployed_tripod_cameras) // add cameras to list o' marines
+ if(current_squad && current_squad.name != "Root")
+ if(!tripod_camera.squad || tripod_camera.squad != current_squad) // tldr: show cameras in root squad if placed by non-squad marines
+ continue
+ if(!tripod_camera.camera || !tripod_camera.camera.can_use()) // skip broken (code) or damaged (in-game) cameras
+ continue // ToDO: There should be an error log if camera is missing camera comp.
+ if(!tripod_camera.loc) // skip null location cameras
+ continue // ToDO: Error Log if camera has no LOC
+ var/turf/camera_turf = get_turf(tripod_camera)
+ if(!camera_turf)
+ continue // ToDO: Error Log if camera has no turf.
+ switch(z_hidden)
+ if(HIDE_ALMAYER)
+ if(is_mainship_level(camera_turf.z))
+ continue
+ if(HIDE_GROUND)
+ if(is_ground_level(camera_turf.z))
+ continue
+ var/area/camera_area = get_area(tripod_camera)
+ var/camera_area_name = camera_area ? sanitize_area(camera_area.name) : "Unknown"
+ var/list/camera_data = list(
+ "name" = tripod_camera.label,
+ "state" = "Active",
+ "has_helmet" = TRUE, // can't click the button in OW if set to false
+ "role" = "Tripod Camera",
+ "acting_sl" = "", // not sure if i need to null these or not
+ "fteam" = "",
+ "distance" = "N/A",
+ "area_name" = camera_area_name,
+ "ref" = REF(tripod_camera),
+ "rank" = "",
+ )
+ data["marines"] += list(camera_data)
+
data["total_deployed"] = leader_count + ftl_count + spec_count + medic_count + engi_count + smart_count + marine_count
data["living_count"] = leaders_alive + ftl_alive + spec_alive + medic_alive + engi_alive + smart_alive + marines_alive
@@ -788,47 +822,62 @@ GLOBAL_LIST_EMPTY_TYPED(active_overwatch_consoles, /obj/structure/machinery/comp
return
if(!params["target_ref"])
return
- if(current_squad)
- var/mob/living/carbon/human/cam_target = locate(params["target_ref"])
+ if(!current_squad)
+ return
- if(!istype(cam_target))
- return
+ var/atom/target_ref = locate(params["target_ref"])
+ var/obj/structure/machinery/camera/new_cam = null
+ var/obj/item/new_holder = null
+ var/atom/cam_target = null
- var/obj/item/new_holder = cam_target.get_camera_holder()
- var/obj/structure/machinery/camera/new_cam
+ if(ishuman(target_ref)) // not strict since synths can be placed in OW squads
+ var/mob/living/carbon/human/Human = target_ref
+ cam_target = Human
+ new_holder = Human.get_camera_holder()
if(new_holder)
new_cam = new_holder.get_camera()
- if(user.interactee != src) //if we multitasking
- user.set_interaction(src)
- if(cam == new_cam) //if we switch to a console that is already watching this cam
- return
- if(!new_cam || !new_cam.can_use())
- to_chat(user, "[icon2html(src, user)] [SPAN_WARNING("Searching for camera. No camera found for this marine! Tell your squad to put their cameras on!")]")
- else if(cam && cam == new_cam)//click the camera you're watching a second time to stop watching.
- visible_message("[icon2html(src, viewers(src))] [SPAN_BOLDNOTICE("Stopping camera view of [cam_target].")]")
- for(var/datum/weakref/user_ref in concurrent_users)
- var/mob/concurrent = user_ref.resolve()
- if(!concurrent)
- continue
- stop_watching_camera(concurrent)
+ else if(istype(target_ref, /obj/structure/overwatch_camera_tripod))
+ var/obj/structure/overwatch_camera_tripod/tripod_camera = target_ref
+ if(tripod_camera.camera)
+ new_cam = tripod_camera.camera
+ cam_target = tripod_camera
+ else
+ to_chat(user, "[icon2html(src, user)] [SPAN_WARNING("Invalid target.")]")
+ return
+
+ if(user.interactee != src) //if we multitasking
+ user.set_interaction(src)
+ if(cam == new_cam) //if we switch to a console that is already watching this cam
+ return
+ if(!new_cam || !new_cam.can_use())
+ to_chat(user, "[icon2html(src, user)] [SPAN_WARNING("Searching for camera. No camera found for this target!")]")
+ else if(cam && cam == new_cam)//click the camera you're watching a second time to stop watching.
+ visible_message("[icon2html(src, viewers(src))] [SPAN_BOLDNOTICE("Stopping camera view.")]")
+ for(var/datum/weakref/user_ref in concurrent_users)
+ var/mob/concurrent = user_ref.resolve()
+ if(!concurrent)
+ continue
+ stop_watching_camera(concurrent)
+ concurrent.UnregisterSignal(cam, COMSIG_PARENT_QDELETING)
+ disconnect_holder()
+ cam = null
+ else if(user.client.view != GLOB.world_view_size)
+ to_chat(user, SPAN_WARNING("You're too busy peering through binoculars."))
+ else
+ for(var/datum/weakref/user_ref in concurrent_users)
+ var/mob/concurrent = user_ref.resolve()
+ if(!concurrent)
+ continue
+ if(cam)
concurrent.UnregisterSignal(cam, COMSIG_PARENT_QDELETING)
- disconnect_holder()
- cam = null
- else if(user.client.view != GLOB.world_view_size)
- to_chat(user, SPAN_WARNING("You're too busy peering through binoculars."))
- else
- for(var/datum/weakref/user_ref in concurrent_users)
- var/mob/concurrent = user_ref.resolve()
- if(!concurrent)
- continue
- if(cam)
- concurrent.UnregisterSignal(cam, COMSIG_PARENT_QDELETING)
- start_watching_camera(concurrent, new_cam)
+ start_watching_camera(concurrent, new_cam)
+ if(cam_target)
set_onscreen_text(concurrent, cam_target)
- concurrent.RegisterSignal(new_cam, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/mob, reset_observer_view_on_deletion))
- if(camera_holder)
- disconnect_holder()
- cam = new_cam
+ concurrent.RegisterSignal(new_cam, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/mob, reset_observer_view_on_deletion))
+ if(camera_holder)
+ disconnect_holder()
+ cam = new_cam
+ if(new_holder)
connect_holder(new_holder)
if("change_operator")
@@ -1580,6 +1629,15 @@ GLOBAL_LIST_EMPTY_TYPED(active_overwatch_consoles, /obj/structure/machinery/comp
watcher.hud_used.overwatch_text.maptext = name_part + location_part + job_part + living_part
+ else if(istype(target, /obj/structure/overwatch_camera_tripod)) // on-screen text - in theory you can't click on a downed camera
+ var/obj/structure/overwatch_camera_tripod/tripod = target
+ var/area/current_area = get_area(tripod)
+ var/area_name = current_area ? sanitize_area(current_area.name) : "Unknown"
+ var/name_part = "[tripod.label]
"
+ var/location_part = "[area_name]
"
+ var/job_part = "Tripod Camera"
+ watcher.hud_used.overwatch_text.maptext = name_part + location_part + job_part
+
/obj/structure/machinery/computer/overwatch/almayer
density = FALSE
icon = 'icons/obj/structures/machinery/computer.dmi'
diff --git a/icons/overwatch.dmi b/icons/overwatch.dmi
new file mode 100644
index 000000000000..9dc21f644143
Binary files /dev/null and b/icons/overwatch.dmi differ