From 86f2489ffffa2cda5ae26227ca8d1ae552c42f30 Mon Sep 17 00:00:00 2001 From: Cthulhu80 <122310258+Cthulhu80@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:57:39 -0500 Subject: [PATCH 1/2] Adds drawing to tacmaps (#4475) # About the pull request Features - Gives cic (sos, xo, co) and the queen the ability to draw on the tactical map and then announce it to their respective factions - All marine faction members now are able to access the tactical map announcement through the stat panel, which can be viewed at any time. - Xenos in addition to having the normal tactical map will also have access to their own canvas map that can be viewed when Queen is on ovi. - Ghosts can view the tactical maps for both factions in the Ghost tab as well as from ghost alert popups. - Admins can view all the round's drawings and optionally delete them. # Explain why it's good for the game Hopefully this will spice things up for cic, queen and players alike. The idea is command and the queen would now be able to better convey what they want from their faction, which should help with general cohesion and likely expand strategic plays on both sides. (don't abuse it or harry will take it away, or make it more exclusive to the queen and co) # Testing photographs and procedure Screen Shot 2023-09-23 at 6 38 25 PM Screen Shot 2023-09-23 at 6 39 08 PM Screen Shot 2023-09-23 at 6 40 22 PM Screen Shot 2023-09-23 at 6 42 54 PM # Changelog :cl: Cthulhu80, Drathek add: Adds drawing to tactical maps, viewable via stat panel for marines and xeno tacmap for xenos. fix: Corrupted (and other hives) now have separate tactical maps. /:cl: --------- Co-authored-by: Drathek <76988376+Drulikar@users.noreply.github.com> Co-authored-by: Drulikar --- code/__DEFINES/hud.dm | 2 + code/__DEFINES/minimap.dm | 18 +- code/__HELPERS/icons.dm | 7 +- code/_globalvars/global_lists.dm | 8 + code/_globalvars/misc.dm | 16 + code/controllers/subsystem/minimap.dm | 491 +++++++++++++++++- code/game/gamemodes/cm_initialize.dm | 26 +- .../game/machinery/computer/communications.dm | 2 +- .../computer/groundside_operations.dm | 6 +- code/game/objects/items/devices/cictablet.dm | 5 +- code/modules/admin/admin_verbs.dm | 1 + .../admin/tacmap_panel/tacmap_admin_panel.dm | 9 + .../tacmap_panel/tacmap_admin_panel_tgui.dm | 152 ++++++ code/modules/almayer/machinery.dm | 13 +- code/modules/client/client_procs.dm | 9 +- .../structures/special/pylon_core.dm | 4 +- code/modules/cm_aliens/structures/tunnel.dm | 2 +- code/modules/cm_marines/overwatch.dm | 5 +- code/modules/cm_preds/yaut_machines.dm | 1 + .../desert_dam/motion_sensor/sensortower.dm | 2 +- code/modules/escape_menu/admin_buttons.dm | 26 +- code/modules/maptext_alerts/screen_alerts.dm | 5 + code/modules/mob/dead/observer/observer.dm | 21 + code/modules/mob/living/carbon/human/human.dm | 4 +- .../mob/living/carbon/xenomorph/Xenomorph.dm | 6 +- .../xenomorph/abilities/queen/queen_powers.dm | 2 +- .../living/carbon/xenomorph/xeno_defines.dm | 3 +- code/modules/mob/new_player/new_player.dm | 10 +- code/modules/vehicles/apc/apc_command.dm | 2 +- colonialmarines.dme | 2 + html/statbrowser.js | 9 + tgui/packages/tgui/interfaces/CanvasLayer.js | 311 +++++++++++ tgui/packages/tgui/interfaces/DrawnMap.js | 110 ++++ .../tgui/interfaces/OverwatchConsole.js | 1 - .../tgui/interfaces/TacmapAdminPanel.js | 164 ++++++ tgui/packages/tgui/interfaces/TacticalMap.tsx | 326 +++++++++++- .../tgui/styles/interfaces/TacticalMap.scss | 4 + 37 files changed, 1720 insertions(+), 65 deletions(-) create mode 100644 code/modules/admin/tacmap_panel/tacmap_admin_panel.dm create mode 100644 code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm create mode 100644 tgui/packages/tgui/interfaces/CanvasLayer.js create mode 100644 tgui/packages/tgui/interfaces/DrawnMap.js create mode 100644 tgui/packages/tgui/interfaces/TacmapAdminPanel.js diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm index 38e5693dcbe5..deee80c7a91d 100644 --- a/code/__DEFINES/hud.dm +++ b/code/__DEFINES/hud.dm @@ -23,3 +23,5 @@ #define NOTIFY_ATTACK "attack" #define NOTIFY_ORBIT "orbit" #define NOTIFY_JOIN_XENO "join_xeno" +#define NOTIFY_XENO_TACMAP "xeno_tacmap" +#define NOTIFY_USCM_TACMAP "uscm_tacmap" diff --git a/code/__DEFINES/minimap.dm b/code/__DEFINES/minimap.dm index 71d0ed8e7445..003d723600c4 100644 --- a/code/__DEFINES/minimap.dm +++ b/code/__DEFINES/minimap.dm @@ -5,7 +5,17 @@ #define MINIMAP_FLAG_UPP (1<<3) #define MINIMAP_FLAG_CLF (1<<4) #define MINIMAP_FLAG_YAUTJA (1<<5) -#define MINIMAP_FLAG_ALL (1<<6) - 1 +#define MINIMAP_FLAG_XENO_CORRUPTED (1<<6) +#define MINIMAP_FLAG_XENO_ALPHA (1<<7) +#define MINIMAP_FLAG_XENO_BRAVO (1<<8) +#define MINIMAP_FLAG_XENO_CHARLIE (1<<9) +#define MINIMAP_FLAG_XENO_DELTA (1<<10) +#define MINIMAP_FLAG_XENO_FERAL (1<<11) +#define MINIMAP_FLAG_XENO_TAMED (1<<12) +#define MINIMAP_FLAG_XENO_MUTATED (1<<13) +#define MINIMAP_FLAG_XENO_FORSAKEN (1<<14) +#define MINIMAP_FLAG_XENO_RENEGADE (1<<15) +#define MINIMAP_FLAG_ALL (1<<16) - 1 ///Converts the overworld x and y to minimap x and y values #define MINIMAP_SCALE 2 @@ -77,9 +87,3 @@ GLOBAL_LIST_INIT(all_minimap_flags, bitfield2list(MINIMAP_FLAG_ALL)) #define TACMAP_BASE_OCCLUDED "Occluded" #define TACMAP_BASE_OPEN "Open" - -#define TACMAP_DEFAULT "Default" -#define TACMAP_XENO "Xeno" -#define TACMAP_YAUTJA "Yautja" -#define TACMAP_FACTION "Faction" - diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 1116f1acb2a8..24e39ff16c89 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -682,8 +682,9 @@ world * * moving - whether or not to use a moving state for the given icon * * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players * * extra_clases - string of extra css classes to use when returning the icon string + * * keyonly - if TRUE, only returns the asset key to use get_asset_url manually. Overrides sourceonly. */ -/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null) +/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null, keyonly = FALSE) if (!thing) return @@ -714,6 +715,8 @@ world SSassets.transport.register_asset(name, thing) for (var/thing2 in targets) SSassets.transport.send_assets(thing2, name) + if(keyonly) + return name if(sourceonly) return SSassets.transport.get_asset_url(name) return "" @@ -756,6 +759,8 @@ world SSassets.transport.register_asset(key, rsc_ref, file_hash, icon_path) for (var/client_target in targets) SSassets.transport.send_assets(client_target, key) + if(keyonly) + return key if(sourceonly) return SSassets.transport.get_asset_url(key) return "" diff --git a/code/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm index 1e1e9cefd5db..3ba92a7c4d0c 100644 --- a/code/_globalvars/global_lists.dm +++ b/code/_globalvars/global_lists.dm @@ -6,6 +6,14 @@ GLOBAL_LIST_EMPTY(CMBFaxes) GLOBAL_LIST_EMPTY(GeneralFaxes) //Inter-machine faxes GLOBAL_LIST_EMPTY(fax_contents) //List of fax contents to maintain it even if source paper is deleted +//datum containing a reference to the flattend map png url, the actual png is stored in the user's cache. +GLOBAL_LIST_EMPTY(uscm_flat_tacmap_data) +GLOBAL_LIST_EMPTY(xeno_flat_tacmap_data) + +//datum containing the svg overlay coords in array format. +GLOBAL_LIST_EMPTY(uscm_svg_tacmap_data) +GLOBAL_LIST_EMPTY(xeno_svg_tacmap_data) + GLOBAL_LIST_EMPTY(failed_fultons) //A list of fultoned items which weren't collected and fell back down GLOBAL_LIST_EMPTY(larva_burst_by_hive) diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm index 44f4b2c4010f..cd6708198eae 100644 --- a/code/_globalvars/misc.dm +++ b/code/_globalvars/misc.dm @@ -14,6 +14,22 @@ GLOBAL_LIST_INIT(pill_icon_mappings, map_pill_icons()) /// In-round override to default OOC color GLOBAL_VAR(ooc_color_override) +// tacmap cooldown for xenos and marines +GLOBAL_VAR_INIT(uscm_canvas_cooldown, 0) +GLOBAL_VAR_INIT(xeno_canvas_cooldown, 0) + +// getFlatIcon cooldown for xenos and marines +GLOBAL_VAR_INIT(uscm_flatten_map_icon_cooldown, 0) +GLOBAL_VAR_INIT(xeno_flatten_map_icon_cooldown, 0) + +// latest unannounced flat tacmap for xenos and marines +GLOBAL_VAR(uscm_unannounced_map) +GLOBAL_VAR(xeno_unannounced_map) + +//global tacmaps for action button access +GLOBAL_DATUM_INIT(uscm_tacmap_status, /datum/tacmap/drawing/status_tab_view, new) +GLOBAL_DATUM_INIT(xeno_tacmap_status, /datum/tacmap/drawing/status_tab_view/xeno, new) + /// List of roles that can be setup for each gamemode GLOBAL_LIST_INIT(gamemode_roles, list()) diff --git a/code/controllers/subsystem/minimap.dm b/code/controllers/subsystem/minimap.dm index 6f5b9303a91f..d28fe916291a 100644 --- a/code/controllers/subsystem/minimap.dm +++ b/code/controllers/subsystem/minimap.dm @@ -256,8 +256,6 @@ SUBSYSTEM_DEF(minimaps) removal_cbs[target] = CALLBACK(src, PROC_REF(removeimage), blip, target) RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(remove_marker)) - - /** * removes an image from raw tracked lists, invoked by callback */ @@ -322,7 +320,7 @@ SUBSYSTEM_DEF(minimaps) minimaps_by_z["[z_level]"].images_assoc["[flag]"] -= source /** - * Fetches a /atom/movable/screen/minimap instance or creates on if none exists + * Fetches a /atom/movable/screen/minimap instance or creates one if none exists * Note this does not destroy them when the map is unused, might be a potential thing to do? * Arguments: * * zlevel: zlevel to fetch map for @@ -338,6 +336,170 @@ SUBSYSTEM_DEF(minimaps) hashed_minimaps[hash] = map return map +/** + * Fetches the datum containing an announced flattend map png reference. + * + * Arguments: + * * faction: FACTION_MARINE or XENO_HIVE_NORMAL + */ +/proc/get_tacmap_data_png(faction) + var/list/map_list + + if(faction == FACTION_MARINE) + map_list = GLOB.uscm_flat_tacmap_data + else if(faction == XENO_HIVE_NORMAL) + map_list = GLOB.xeno_flat_tacmap_data + else + return null + + var/map_length = length(map_list) + + if(map_length == 0) + return null + + return map_list[map_length] + +/** + * Fetches the datum containing the latest unannounced flattend map png reference. + * + * Arguments: + * * faction: FACTION_MARINE or XENO_HIVE_NORMAL + */ +/proc/get_unannounced_tacmap_data_png(faction) + if(faction == FACTION_MARINE) + return GLOB.uscm_unannounced_map + else if(faction == XENO_HIVE_NORMAL) + return GLOB.xeno_unannounced_map + + return null + +/** + * Fetches the last set of svg coordinates for the tacmap drawing. + * + * Arguments: + * * faction: which faction get the map for: FACTION_MARINE or XENO_HIVE_NORMAL + */ +/proc/get_tacmap_data_svg(faction) + var/list/map_list + + if(faction == FACTION_MARINE) + map_list = GLOB.uscm_svg_tacmap_data + else if(faction == XENO_HIVE_NORMAL) + map_list = GLOB.xeno_svg_tacmap_data + else + return null + + var/map_length = length(map_list) + + if(map_length == 0) + return null + + return map_list[map_length] + +/** + * Re-sends relevant flattened tacmaps to a single client. + * + * Arguments: + * * user: The mob that is either an observer, marine, or xeno + */ +/proc/resend_current_map_png(mob/user) + if(!user.client) + return + + var/is_observer = user.faction == FACTION_NEUTRAL && isobserver(user) + if(is_observer || user.faction == FACTION_MARINE) + // Send marine maps + var/datum/flattened_tacmap/latest = get_tacmap_data_png(FACTION_MARINE) + if(latest) + SSassets.transport.send_assets(user.client, latest.asset_key) + var/datum/flattened_tacmap/unannounced = get_unannounced_tacmap_data_png(FACTION_MARINE) + if(unannounced && (!latest || latest.asset_key != unannounced.asset_key)) + SSassets.transport.send_assets(user.client, unannounced.asset_key) + + var/mob/living/carbon/xenomorph/xeno = user + if(is_observer || istype(xeno) && xeno.hivenumber == XENO_HIVE_NORMAL) + // Send xeno maps + var/datum/flattened_tacmap/latest = get_tacmap_data_png(XENO_HIVE_NORMAL) + if(latest) + SSassets.transport.send_assets(user.client, latest.asset_key) + var/datum/flattened_tacmap/unannounced = get_unannounced_tacmap_data_png(XENO_HIVE_NORMAL) + if(unannounced && (!latest || latest.asset_key != unannounced.asset_key)) + SSassets.transport.send_assets(user.client, unannounced.asset_key) + +/** + * Flattens the current map and then distributes it for the specified faction as an unannounced map. + * + * Arguments: + * * faction: Which faction to distribute the map to: FACTION_MARINE or XENO_HIVE_NORMAL + * Return: + * * Returns a boolean value, TRUE if the operation was successful, FALSE if it was not (on cooldown generally). + */ +/datum/tacmap/drawing/proc/distribute_current_map_png(faction) + if(faction == FACTION_MARINE) + if(!COOLDOWN_FINISHED(GLOB, uscm_flatten_map_icon_cooldown)) + return FALSE + COOLDOWN_START(GLOB, uscm_flatten_map_icon_cooldown, flatten_map_cooldown_time) + else if(faction == XENO_HIVE_NORMAL) + if(!COOLDOWN_FINISHED(GLOB, xeno_flatten_map_icon_cooldown)) + return FALSE + COOLDOWN_START(GLOB, xeno_flatten_map_icon_cooldown, flatten_map_cooldown_time) + else + return FALSE + + var/icon/flat_map = getFlatIcon(map_holder.map, appearance_flags = TRUE) + if(!flat_map) + to_chat(usr, SPAN_WARNING("A critical error has occurred! Contact a coder.")) // tf2heavy: "Oh, this is bad!" + return FALSE + + // Send to only relevant clients + var/list/faction_clients = list() + for(var/client/client as anything in GLOB.clients) + if(!client || !client.mob) + continue + var/mob/client_mob = client.mob + if(client_mob.faction == faction) + faction_clients += client + else if(client_mob.faction == FACTION_NEUTRAL && isobserver(client_mob)) + faction_clients += client + else if(isxeno(client_mob)) + var/mob/living/carbon/xenomorph/xeno = client_mob + if(xeno.hivenumber == faction) + faction_clients += client + + // This may be unnecessary to do this way if the asset url is always the same as the lookup key + var/flat_tacmap_key = icon2html(flat_map, faction_clients, keyonly = TRUE) + if(!flat_tacmap_key) + to_chat(usr, SPAN_WARNING("A critical error has occurred! Contact a coder.")) + return FALSE + var/flat_tacmap_png = SSassets.transport.get_asset_url(flat_tacmap_key) + var/datum/flattened_tacmap/new_flat = new(flat_tacmap_png, flat_tacmap_key) + + if(faction == FACTION_MARINE) + GLOB.uscm_unannounced_map = new_flat + else //if(faction == XENO_HIVE_NORMAL) + GLOB.xeno_unannounced_map = new_flat + + return TRUE + +/** + * Globally stores svg coords for a given faction. + * + * Arguments: + * * faction: which faction to save the data for: FACTION_MARINE or XENO_HIVE_NORMAL + * * svg_coords: an array of coordinates corresponding to an svg. + * * ckey: the ckey of the user who submitted this + */ +/datum/tacmap/drawing/proc/store_current_svg_coords(faction, svg_coords, ckey) + var/datum/svg_overlay/svg_store_overlay = new(svg_coords, ckey) + + if(faction == FACTION_MARINE) + GLOB.uscm_svg_tacmap_data += svg_store_overlay + else if(faction == XENO_HIVE_NORMAL) + GLOB.xeno_svg_tacmap_data += svg_store_overlay + else + qdel(svg_store_overlay) + debug_log("SVG coordinates for [faction] are not implemented!") + /datum/controller/subsystem/minimaps/proc/fetch_tacmap_datum(zlevel, flags) var/hash = "[zlevel]-[flags]" if(hashed_tacmaps[hash]) @@ -442,7 +604,7 @@ SUBSYSTEM_DEF(minimaps) marker_flags = MINIMAP_FLAG_USCM /datum/action/minimap/observer - minimap_flags = MINIMAP_FLAG_XENO|MINIMAP_FLAG_USCM|MINIMAP_FLAG_UPP|MINIMAP_FLAG_CLF|MINIMAP_FLAG_UPP + minimap_flags = MINIMAP_FLAG_ALL marker_flags = NONE hidden = TRUE @@ -452,17 +614,61 @@ SUBSYSTEM_DEF(minimaps) var/targeted_ztrait = ZTRAIT_GROUND var/atom/owner + /// tacmap holder for holding the minimap var/datum/tacmap_holder/map_holder +/datum/tacmap/drawing + /// A url that will point to the wiki map for the current map as a fall back image + var/static/wiki_map_fallback + + /// color selection for the tactical map canvas, defaults to black. + var/toolbar_color_selection = "black" + var/toolbar_updated_selection = "black" + + var/canvas_cooldown_time = 4 MINUTES + var/flatten_map_cooldown_time = 3 MINUTES + + /// boolean value to keep track if the canvas has been updated or not, the value is used in tgui state. + var/updated_canvas = FALSE + /// current flattend map + var/datum/flattened_tacmap/new_current_map + /// previous flattened map + var/datum/flattened_tacmap/old_map + /// current svg + var/datum/svg_overlay/current_svg + + var/action_queue_change = 0 + + /// The last time the map has been flattened - used as a key to trick react into updating the canvas + var/last_update_time = 0 + /// A temporary lock out time before we can open the new canvas tab to allow the tacmap time to fire + var/tacmap_ready_time = 0 + /datum/tacmap/New(atom/source, minimap_type) allowed_flags = minimap_type owner = source +/datum/tacmap/drawing/status_tab_view/New() + var/datum/tacmap/drawing/status_tab_view/uscm_tacmap + allowed_flags = MINIMAP_FLAG_USCM + owner = uscm_tacmap + +/datum/tacmap/drawing/status_tab_view/xeno/New() + var/datum/tacmap/drawing/status_tab_view/xeno/xeno_tacmap + allowed_flags = MINIMAP_FLAG_XENO + owner = xeno_tacmap + /datum/tacmap/Destroy() map_holder = null owner = null return ..() +/datum/tacmap/drawing/Destroy() + new_current_map = null + old_map = null + current_svg = null + return ..() + /datum/tacmap/tgui_interact(mob/user, datum/tgui/ui) if(!map_holder) var/level = SSmapping.levels_by_trait(targeted_ztrait) @@ -476,11 +682,216 @@ SUBSYSTEM_DEF(minimaps) ui = new(user, src, "TacticalMap") ui.open() +/datum/tacmap/drawing/tgui_interact(mob/user, datum/tgui/ui) + var/mob/living/carbon/xenomorph/xeno = user + var/is_xeno = istype(xeno) + var/faction = is_xeno ? xeno.hivenumber : user.faction + if(faction == FACTION_NEUTRAL && isobserver(user)) + faction = allowed_flags == MINIMAP_FLAG_XENO ? XENO_HIVE_NORMAL : FACTION_MARINE + + new_current_map = get_unannounced_tacmap_data_png(faction) + old_map = get_tacmap_data_png(faction) + current_svg = get_tacmap_data_svg(faction) + + var/use_live_map = faction == FACTION_MARINE && skillcheck(user, SKILL_LEADERSHIP, SKILL_LEAD_EXPERT) || is_xeno + + if(use_live_map && !map_holder) + var/level = SSmapping.levels_by_trait(targeted_ztrait) + if(!level[1]) + return + map_holder = SSminimaps.fetch_tacmap_datum(level[1], allowed_flags) + + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + if(!wiki_map_fallback) + var/wiki_url = CONFIG_GET(string/wikiurl) + var/obj/item/map/current_map/new_map = new + if(wiki_url && new_map.html_link) + wiki_map_fallback ="[wiki_url]/[new_map.html_link]" + else + debug_log("Failed to determine fallback wiki map! Attempted '[wiki_url]/[new_map.html_link]'") + qdel(new_map) + + // Ensure we actually have the map image sent + resend_current_map_png(user) + + if(use_live_map) + tacmap_ready_time = SSminimaps.next_fire + 2 SECONDS + addtimer(CALLBACK(src, PROC_REF(on_tacmap_fire), faction), SSminimaps.next_fire - world.time + 1 SECONDS) + user.client.register_map_obj(map_holder.map) + + ui = new(user, src, "TacticalMap") + ui.open() + +/datum/tacmap/drawing/ui_data(mob/user) + var/list/data = list() + + data["newCanvasFlatImage"] = new_current_map?.flat_tacmap + data["oldCanvasFlatImage"] = old_map?.flat_tacmap + data["svgData"] = current_svg?.svg_data + + data["actionQueueChange"] = action_queue_change + + data["toolbarColorSelection"] = toolbar_color_selection + data["toolbarUpdatedSelection"] = toolbar_updated_selection + + if(isxeno(user)) + data["canvasCooldown"] = max(GLOB.xeno_canvas_cooldown - world.time, 0) + else + data["canvasCooldown"] = max(GLOB.uscm_canvas_cooldown - world.time, 0) + + data["nextCanvasTime"] = canvas_cooldown_time + data["updatedCanvas"] = updated_canvas + + data["lastUpdateTime"] = last_update_time + data["tacmapReady"] = world.time > tacmap_ready_time + + return data + /datum/tacmap/ui_static_data(mob/user) var/list/data = list() - data["mapRef"] = map_holder.map_ref + data["mapRef"] = map_holder?.map_ref + data["canDraw"] = FALSE + data["canViewTacmap"] = TRUE + data["canViewCanvas"] = FALSE + data["isXeno"] = FALSE + return data +/datum/tacmap/drawing/ui_static_data(mob/user) + var/list/data = list() + + data["mapRef"] = map_holder?.map_ref + data["canDraw"] = FALSE + data["mapFallback"] = wiki_map_fallback + + var/mob/living/carbon/xenomorph/xeno = user + var/is_xeno = istype(xeno) + var/faction = is_xeno ? xeno.hivenumber : user.faction + + data["isXeno"] = is_xeno + data["canViewTacmap"] = is_xeno + data["canViewCanvas"] = faction == FACTION_MARINE || faction == XENO_HIVE_NORMAL + + if(faction == FACTION_MARINE && skillcheck(user, SKILL_LEADERSHIP, SKILL_LEAD_EXPERT) || faction == XENO_HIVE_NORMAL && isqueen(user)) + data["canDraw"] = TRUE + data["canViewTacmap"] = TRUE + + return data + +/datum/tacmap/drawing/status_tab_view/ui_static_data(mob/user) + var/list/data = list() + data["mapFallback"] = wiki_map_fallback + data["canDraw"] = FALSE + data["canViewTacmap"] = FALSE + data["canViewCanvas"] = TRUE + data["isXeno"] = FALSE + + return data + +/datum/tacmap/drawing/status_tab_view/xeno/ui_static_data(mob/user) + var/list/data = list() + data["mapFallback"] = wiki_map_fallback + data["canDraw"] = FALSE + data["canViewTacmap"] = FALSE + data["canViewCanvas"] = TRUE + data["isXeno"] = TRUE + + return data + +/datum/tacmap/drawing/ui_close(mob/user) + . = ..() + action_queue_change = 0 + updated_canvas = FALSE + toolbar_color_selection = "black" + toolbar_updated_selection = "black" + +/datum/tacmap/drawing/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + var/mob/user = ui.user + var/mob/living/carbon/xenomorph/xeno = user + var/faction = istype(xeno) ? xeno.hivenumber : user.faction + if(faction == FACTION_NEUTRAL && isobserver(user)) + faction = allowed_flags == MINIMAP_FLAG_XENO ? XENO_HIVE_NORMAL : FACTION_MARINE + + switch (action) + if ("menuSelect") + if(params["selection"] != "new canvas") + if(updated_canvas) + updated_canvas = FALSE + toolbar_updated_selection = toolbar_color_selection // doing this if it == canvas can cause a latency issue with the stroke. + else + distribute_current_map_png(faction) + last_update_time = world.time + // An attempt to get the image to load on first try in the interface, but doesn't seem always reliable + + new_current_map = get_unannounced_tacmap_data_png(faction) + old_map = get_tacmap_data_png(faction) + current_svg = get_tacmap_data_svg(faction) + + if ("updateCanvas") + // forces state change, this will export the svg. + toolbar_updated_selection = "export" + updated_canvas = TRUE + action_queue_change += 1 + + if ("clearCanvas") + toolbar_updated_selection = "clear" + updated_canvas = FALSE + action_queue_change += 1 + + if ("undoChange") + toolbar_updated_selection = "undo" + updated_canvas = FALSE + action_queue_change += 1 + + if ("selectColor") + var/newColor = params["color"] + if(newColor) + toolbar_color_selection = newColor + toolbar_updated_selection = newColor + action_queue_change += 1 + + if ("onDraw") + updated_canvas = FALSE + + if ("selectAnnouncement") + if(!istype(params["image"], /list)) // potentially very serious? + return FALSE + + if(faction == FACTION_MARINE) + GLOB.uscm_flat_tacmap_data += new_current_map + else if(faction == XENO_HIVE_NORMAL) + GLOB.xeno_flat_tacmap_data += new_current_map + + store_current_svg_coords(faction, params["image"], user) + current_svg = get_tacmap_data_svg(faction) + old_map = get_tacmap_data_png(faction) + + if(faction == FACTION_MARINE) + COOLDOWN_START(GLOB, uscm_canvas_cooldown, canvas_cooldown_time) + var/mob/living/carbon/human/human_leader = user + for(var/datum/squad/current_squad in RoleAuthority.squads) + current_squad.send_maptext("Tactical map update in progress...", "Tactical Map:") + human_leader.visible_message(SPAN_BOLDNOTICE("Tactical map update in progress...")) + playsound_client(human_leader.client, "sound/effects/sos-morse-code.ogg") + notify_ghosts(header = "Tactical Map", message = "The USCM tactical map has been updated.", ghost_sound = "sound/effects/sos-morse-code.ogg", notify_volume = 80, action = NOTIFY_USCM_TACMAP, enter_link = "uscm_tacmap=1", enter_text = "View", source = owner) + + else if(faction == XENO_HIVE_NORMAL) + var/mutable_appearance/appearance = mutable_appearance(icon('icons/mob/hud/actions_xeno.dmi'), "toggle_queen_zoom") + COOLDOWN_START(GLOB, xeno_canvas_cooldown, canvas_cooldown_time) + xeno_maptext("The Queen has updated your hive mind map", "You sense something unusual...", faction) + notify_ghosts(header = "Tactical Map", message = "The Xenomorph tactical map has been updated.", ghost_sound = "sound/voice/alien_distantroar_3.ogg", notify_volume = 50, action = NOTIFY_XENO_TACMAP, enter_link = "xeno_tacmap=1", enter_text = "View", source = user, alert_overlay = appearance) + + toolbar_updated_selection = toolbar_color_selection + message_admins("[key_name(user)] has updated the tactical map for [faction].") + updated_canvas = FALSE + + return TRUE + /datum/tacmap/ui_status(mob/user) if(!(isatom(owner))) return UI_INTERACTIVE @@ -493,7 +904,7 @@ SUBSYSTEM_DEF(minimaps) else return UI_CLOSE -/datum/tacmap/xeno/ui_status(mob/user) +/datum/tacmap/drawing/xeno/ui_status(mob/user) if(!isxeno(user)) return UI_CLOSE @@ -516,3 +927,71 @@ SUBSYSTEM_DEF(minimaps) /datum/tacmap_holder/Destroy() map = null return ..() + +/datum/flattened_tacmap + var/flat_tacmap + var/asset_key + var/time + +/datum/flattened_tacmap/New(flat_tacmap, asset_key) + src.flat_tacmap = flat_tacmap + src.asset_key = asset_key + src.time = time_stamp() + +/datum/svg_overlay + var/svg_data + var/ckey + var/name + var/time + +/datum/svg_overlay/New(svg_data, mob/user) + src.svg_data = svg_data + src.ckey = user?.persistent_ckey + src.name = user?.real_name + src.time = time_stamp() + +/// Callback when timer indicates the tacmap is flattenable now +/datum/tacmap/drawing/proc/on_tacmap_fire(faction) + distribute_current_map_png(faction) + last_update_time = world.time + +/// Gets the MINIMAP_FLAG for the provided faction or hivenumber if one exists +/proc/get_minimap_flag_for_faction(faction) + switch(faction) + if(XENO_HIVE_NORMAL) + return MINIMAP_FLAG_XENO + if(FACTION_MARINE) + return MINIMAP_FLAG_USCM + if(FACTION_UPP) + return MINIMAP_FLAG_UPP + if(FACTION_WY) + return MINIMAP_FLAG_USCM + if(FACTION_CLF) + return MINIMAP_FLAG_CLF + if(FACTION_PMC) + return MINIMAP_FLAG_PMC + if(FACTION_YAUTJA) + return MINIMAP_FLAG_YAUTJA + if(XENO_HIVE_CORRUPTED) + return MINIMAP_FLAG_XENO_CORRUPTED + if(XENO_HIVE_ALPHA) + return MINIMAP_FLAG_XENO_ALPHA + if(XENO_HIVE_BRAVO) + return MINIMAP_FLAG_XENO_BRAVO + if(XENO_HIVE_CHARLIE) + return MINIMAP_FLAG_XENO_CHARLIE + if(XENO_HIVE_DELTA) + return MINIMAP_FLAG_XENO_DELTA + if(XENO_HIVE_FERAL) + return MINIMAP_FLAG_XENO_FERAL + if(XENO_HIVE_TAMED) + return MINIMAP_FLAG_XENO_TAMED + if(XENO_HIVE_MUTATED) + return MINIMAP_FLAG_XENO_MUTATED + if(XENO_HIVE_FORSAKEN) + return MINIMAP_FLAG_XENO_FORSAKEN + if(XENO_HIVE_YAUTJA) + return MINIMAP_FLAG_YAUTJA + if(XENO_HIVE_RENEGADE) + return MINIMAP_FLAG_XENO_RENEGADE + return 0 diff --git a/code/game/gamemodes/cm_initialize.dm b/code/game/gamemodes/cm_initialize.dm index effd3325f887..17a255009089 100644 --- a/code/game/gamemodes/cm_initialize.dm +++ b/code/game/gamemodes/cm_initialize.dm @@ -423,7 +423,7 @@ Additional game mode variables. for(var/mob_name in picked_hive.banished_ckeys) if(picked_hive.banished_ckeys[mob_name] == xeno_candidate.ckey) to_chat(xeno_candidate, SPAN_WARNING("You are banished from the [picked_hive], you may not rejoin unless the Queen re-admits you or dies.")) - return + return FALSE if(isnewplayer(xeno_candidate)) var/mob/new_player/noob = xeno_candidate noob.close_spawn_windows() @@ -443,9 +443,6 @@ Additional game mode variables. return FALSE new_xeno = userInput - if(!xeno_candidate) - return FALSE - if(!(new_xeno in GLOB.living_xeno_list) || new_xeno.stat == DEAD) to_chat(xeno_candidate, SPAN_WARNING("You cannot join if the xenomorph is dead.")) return FALSE @@ -479,14 +476,14 @@ Additional game mode variables. else new_xeno = pick(available_xenos_non_ssd) //Just picks something at random. if(istype(new_xeno) && xeno_candidate && xeno_candidate.client) if(isnewplayer(xeno_candidate)) - var/mob/new_player/N = xeno_candidate - N.close_spawn_windows() + var/mob/new_player/noob = xeno_candidate + noob.close_spawn_windows() for(var/mob_name in new_xeno.hive.banished_ckeys) if(new_xeno.hive.banished_ckeys[mob_name] == xeno_candidate.ckey) to_chat(xeno_candidate, SPAN_WARNING("You are banished from this hive, You may not rejoin unless the Queen re-admits you or dies.")) - return + return FALSE if(transfer_xeno(xeno_candidate, new_xeno)) - return 1 + return TRUE to_chat(xeno_candidate, "JAS01: Something went wrong, tell a coder.") /datum/game_mode/proc/attempt_to_join_as_facehugger(mob/xeno_candidate) @@ -614,20 +611,21 @@ Additional game mode variables. /datum/game_mode/proc/transfer_xeno(xeno_candidate, mob/living/new_xeno) if(!xeno_candidate || !isxeno(new_xeno) || QDELETED(new_xeno)) return FALSE + var/datum/mind/xeno_candidate_mind if(ismind(xeno_candidate)) xeno_candidate_mind = xeno_candidate else if(ismob(xeno_candidate)) - var/mob/M = xeno_candidate - if(M.mind) - xeno_candidate_mind = M.mind + var/mob/xeno_candidate_mob = xeno_candidate + if(xeno_candidate_mob.mind) + xeno_candidate_mind = xeno_candidate_mob.mind else - xeno_candidate_mind = new /datum/mind(M.key, M.ckey) + xeno_candidate_mind = new /datum/mind(xeno_candidate_mob.key, xeno_candidate_mob.ckey) xeno_candidate_mind.active = TRUE xeno_candidate_mind.current = new_xeno else if(isclient(xeno_candidate)) - var/client/C = xeno_candidate - xeno_candidate_mind = new /datum/mind(C.key, C.ckey) + var/client/xeno_candidate_client = xeno_candidate + xeno_candidate_mind = new /datum/mind(xeno_candidate_client.key, xeno_candidate_client.ckey) xeno_candidate_mind.active = TRUE xeno_candidate_mind.current = new_xeno else diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index 3332577683fe..b39f59530adc 100644 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -43,7 +43,7 @@ var/stat_msg1 var/stat_msg2 - var/datum/tacmap/tacmap + var/datum/tacmap/drawing/tacmap var/minimap_type = MINIMAP_FLAG_USCM processing = TRUE diff --git a/code/game/machinery/computer/groundside_operations.dm b/code/game/machinery/computer/groundside_operations.dm index 9856ae8f970e..f2b36276c8eb 100644 --- a/code/game/machinery/computer/groundside_operations.dm +++ b/code/game/machinery/computer/groundside_operations.dm @@ -25,7 +25,11 @@ add_pmcs = FALSE else if(SSticker.current_state < GAME_STATE_PLAYING) RegisterSignal(SSdcs, COMSIG_GLOB_MODE_PRESETUP, PROC_REF(disable_pmc)) - tacmap = new(src, minimap_type) + if(announcement_faction == FACTION_MARINE) + tacmap = new /datum/tacmap/drawing(src, minimap_type) + else + tacmap = new(src, minimap_type) // Non-drawing version + return ..() /obj/structure/machinery/computer/groundside_operations/Destroy() diff --git a/code/game/objects/items/devices/cictablet.dm b/code/game/objects/items/devices/cictablet.dm index 6abd70980136..69e745da0803 100644 --- a/code/game/objects/items/devices/cictablet.dm +++ b/code/game/objects/items/devices/cictablet.dm @@ -24,7 +24,10 @@ COOLDOWN_DECLARE(distress_cooldown) /obj/item/device/cotablet/Initialize() - tacmap = new(src, minimap_type) + if(announcement_faction == FACTION_MARINE) + tacmap = new /datum/tacmap/drawing(src, minimap_type) + else + tacmap = new(src, minimap_type) // Non-drawing version if(SSticker.mode && MODE_HAS_FLAG(MODE_FACTION_CLASH)) add_pmcs = FALSE else if(SSticker.current_state < GAME_STATE_PLAYING) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 7d9127313094..5d02917f70ee 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -69,6 +69,7 @@ var/list/admin_verbs_default = list( /client/proc/toggle_ares_ping, /client/proc/cmd_admin_say, /*staff-only ooc chat*/ /client/proc/cmd_mod_say, /* alternate way of typing asay, no different than cmd_admin_say */ + /client/proc/cmd_admin_tacmaps_panel, ) var/list/admin_verbs_admin = list( diff --git a/code/modules/admin/tacmap_panel/tacmap_admin_panel.dm b/code/modules/admin/tacmap_panel/tacmap_admin_panel.dm new file mode 100644 index 000000000000..dcc8c7d5b664 --- /dev/null +++ b/code/modules/admin/tacmap_panel/tacmap_admin_panel.dm @@ -0,0 +1,9 @@ +/client/proc/cmd_admin_tacmaps_panel() + set name = "Tacmaps Panel" + set category = "Admin.Panels" + + if(!check_rights(R_ADMIN|R_MOD)) + to_chat(src, "Only administrators may use this command.") + return + + GLOB.tacmap_admin_panel.tgui_interact(mob) diff --git a/code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm b/code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm new file mode 100644 index 000000000000..e4b6f6846031 --- /dev/null +++ b/code/modules/admin/tacmap_panel/tacmap_admin_panel_tgui.dm @@ -0,0 +1,152 @@ +GLOBAL_DATUM_INIT(tacmap_admin_panel, /datum/tacmap_admin_panel, new) + +#define LATEST_SELECTION -1 + +/datum/tacmap_admin_panel + var/name = "Tacmap Panel" + /// The index picked last for USCM (zero indexed), -1 will try to select latest if it exists + var/uscm_selection = LATEST_SELECTION + /// The index picked last for Xenos (zero indexed), -1 will try to select latest if it exists + var/xeno_selection = LATEST_SELECTION + /// A url that will point to the wiki map for the current map as a fall back image + var/static/wiki_map_fallback + /// The last time the map selection was changed - used as a key to trick react into updating the map + var/last_update_time = 0 + +/datum/tacmap_admin_panel/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + if(!wiki_map_fallback) + var/wiki_url = CONFIG_GET(string/wikiurl) + var/obj/item/map/current_map/new_map = new + if(wiki_url && new_map.html_link) + wiki_map_fallback ="[wiki_url]/[new_map.html_link]" + else + debug_log("Failed to determine fallback wiki map! Attempted '[wiki_url]/[new_map.html_link]'") + qdel(new_map) + + // Ensure we actually have the latest map images sent (recache can handle older/different faction maps) + resend_current_map_png(user) + + ui = new(user, src, "TacmapAdminPanel", "Tacmap Panel") + ui.open() + +/datum/tacmap_admin_panel/ui_state(mob/user) + return GLOB.admin_state + +/datum/tacmap_admin_panel/ui_data(mob/user) + var/list/data = list() + var/list/uscm_ckeys = list() + var/list/xeno_ckeys = list() + var/list/uscm_names = list() + var/list/xeno_names = list() + var/list/uscm_times = list() + var/list/xeno_times = list() + + // Assumption: Length of flat_tacmap_data is the same as svg_tacmap_data + var/uscm_length = length(GLOB.uscm_svg_tacmap_data) + if(uscm_selection < 0 || uscm_selection >= uscm_length) + uscm_selection = uscm_length - 1 + for(var/i = 1, i <= uscm_length, i++) + var/datum/svg_overlay/current_svg = GLOB.uscm_svg_tacmap_data[i] + uscm_ckeys += current_svg.ckey + uscm_names += current_svg.name + uscm_times += current_svg.time + data["uscm_ckeys"] = uscm_ckeys + data["uscm_names"] = uscm_names + data["uscm_times"] = uscm_times + + var/xeno_length = length(GLOB.xeno_svg_tacmap_data) + if(xeno_selection < 0 || xeno_selection >= xeno_length) + xeno_selection = xeno_length - 1 + for(var/i = 1, i <= xeno_length, i++) + var/datum/svg_overlay/current_svg = GLOB.xeno_svg_tacmap_data[i] + xeno_ckeys += current_svg.ckey + xeno_names += current_svg.name + xeno_times += current_svg.time + data["xeno_ckeys"] = xeno_ckeys + data["xeno_names"] = xeno_names + data["xeno_times"] = xeno_times + + if(uscm_selection == LATEST_SELECTION) + data["uscm_map"] = null + data["uscm_svg"] = null + else + var/datum/flattened_tacmap/selected_flat = GLOB.uscm_flat_tacmap_data[uscm_selection + 1] + var/datum/svg_overlay/selected_svg = GLOB.uscm_svg_tacmap_data[uscm_selection + 1] + data["uscm_map"] = selected_flat.flat_tacmap + data["uscm_svg"] = selected_svg.svg_data + + if(xeno_selection == LATEST_SELECTION) + data["xeno_map"] = null + data["xeno_svg"] = null + else + var/datum/flattened_tacmap/selected_flat = GLOB.xeno_flat_tacmap_data[xeno_selection + 1] + var/datum/svg_overlay/selected_svg = GLOB.xeno_svg_tacmap_data[xeno_selection + 1] + data["xeno_map"] = selected_flat.flat_tacmap + data["xeno_svg"] = selected_svg.svg_data + + data["uscm_selection"] = uscm_selection + data["xeno_selection"] = xeno_selection + data["map_fallback"] = wiki_map_fallback + data["last_update_time"] = last_update_time + + return data + +/datum/tacmap_admin_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + var/mob/user = ui.user + var/client/client_user = user.client + if(!client_user) + return // Is this even possible? + + switch(action) + if("recache") + var/is_uscm = params["uscm"] + var/datum/flattened_tacmap/selected_flat + if(is_uscm) + if(uscm_selection == LATEST_SELECTION) + return TRUE + selected_flat = GLOB.uscm_flat_tacmap_data[uscm_selection + 1] + else + if(xeno_selection == LATEST_SELECTION) + return TRUE + selected_flat = GLOB.xeno_flat_tacmap_data[xeno_selection + 1] + SSassets.transport.send_assets(client_user, selected_flat.asset_key) + last_update_time = world.time + return TRUE + + if("change_selection") + var/is_uscm = params["uscm"] + if(is_uscm) + uscm_selection = params["index"] + else + xeno_selection = params["index"] + last_update_time = world.time + return TRUE + + if("delete") + var/is_uscm = params["uscm"] + var/datum/svg_overlay/selected_svg + if(is_uscm) + if(uscm_selection == LATEST_SELECTION) + return TRUE + selected_svg = GLOB.uscm_svg_tacmap_data[uscm_selection + 1] + else + if(xeno_selection == LATEST_SELECTION) + return TRUE + selected_svg = GLOB.xeno_svg_tacmap_data[xeno_selection + 1] + selected_svg.svg_data = null + last_update_time = world.time + message_admins("[key_name_admin(usr)] deleted the tactical map drawing by [selected_svg.ckey].") + return TRUE + +/datum/tacmap_admin_panel/ui_close(mob/user) + . = ..() + uscm_selection = LATEST_SELECTION + xeno_selection = LATEST_SELECTION + +#undef LATEST_SELECTION diff --git a/code/modules/almayer/machinery.dm b/code/modules/almayer/machinery.dm index cb90db9e8535..e72f4e7f9f52 100644 --- a/code/modules/almayer/machinery.dm +++ b/code/modules/almayer/machinery.dm @@ -80,13 +80,19 @@ use_power = USE_POWER_IDLE density = TRUE idle_power_usage = 2 - ///flags that we want to be shown when you interact with this table var/datum/tacmap/map + ///flags that we want to be shown when you interact with this table var/minimap_type = MINIMAP_FLAG_USCM + ///The faction that is intended to use this structure (determines type of tacmap used) + var/faction = FACTION_MARINE /obj/structure/machinery/prop/almayer/CICmap/Initialize() . = ..() - map = new(src, minimap_type) + + if (faction == FACTION_MARINE) + map = new /datum/tacmap/drawing(src, minimap_type) + else + map = new(src, minimap_type) // Non-drawing version /obj/structure/machinery/prop/almayer/CICmap/Destroy() QDEL_NULL(map) @@ -99,12 +105,15 @@ /obj/structure/machinery/prop/almayer/CICmap/upp minimap_type = MINIMAP_FLAG_UPP + faction = FACTION_UPP /obj/structure/machinery/prop/almayer/CICmap/clf minimap_type = MINIMAP_FLAG_CLF + faction = FACTION_CLF /obj/structure/machinery/prop/almayer/CICmap/pmc minimap_type = MINIMAP_FLAG_PMC + faction = FACTION_PMC //Nonpower using props diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 9268cd3aadba..7811a6c9cb5a 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -147,7 +147,6 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list( return cmd_admin_pm(receiver_client, null) return - else if(href_list["FaxView"]) var/datum/fax/info = locate(href_list["FaxView"]) @@ -164,6 +163,14 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list( else if(href_list["medals_panel"]) GLOB.medals_panel.tgui_interact(mob) + else if(href_list["tacmaps_panel"]) + GLOB.tacmap_admin_panel.tgui_interact(mob) + + else if(href_list["MapView"]) + if(isxeno(mob)) + return + GLOB.uscm_tacmap_status.tgui_interact(mob) + //NOTES OVERHAUL if(href_list["add_merit_info"]) var/key = href_list["add_merit_info"] diff --git a/code/modules/cm_aliens/structures/special/pylon_core.dm b/code/modules/cm_aliens/structures/special/pylon_core.dm index 88964d76c3eb..62a7417c57f8 100644 --- a/code/modules/cm_aliens/structures/special/pylon_core.dm +++ b/code/modules/cm_aliens/structures/special/pylon_core.dm @@ -248,7 +248,7 @@ /obj/effect/alien/resin/special/pylon/core/proc/update_minimap_icon() SSminimaps.remove_marker(src) - SSminimaps.add_marker(src, z, MINIMAP_FLAG_XENO, "core") + SSminimaps.add_marker(src, z, get_minimap_flag_for_faction(linked_hive?.hivenumber), "core") /obj/effect/alien/resin/special/pylon/core/process() . = ..() @@ -318,7 +318,7 @@ to_chat(new_xeno, SPAN_XENOANNOUNCE("You are a xenomorph larva awakened from slumber!")) playsound(new_xeno, 'sound/effects/xeno_newlarva.ogg', 50, 1) if(new_xeno.client) - if(new_xeno.client?.prefs.toggles_flashing & FLASH_POOLSPAWN) + if(new_xeno.client.prefs.toggles_flashing & FLASH_POOLSPAWN) window_flash(new_xeno.client) linked_hive.stored_larva-- diff --git a/code/modules/cm_aliens/structures/tunnel.dm b/code/modules/cm_aliens/structures/tunnel.dm index f716d69b5b7e..185bee06c513 100644 --- a/code/modules/cm_aliens/structures/tunnel.dm +++ b/code/modules/cm_aliens/structures/tunnel.dm @@ -48,7 +48,7 @@ if(resin_trap) qdel(resin_trap) - SSminimaps.add_marker(src, z, MINIMAP_FLAG_XENO, "xenotunnel") + SSminimaps.add_marker(src, z, get_minimap_flag_for_faction(hivenumber), "xenotunnel") /obj/structure/tunnel/Destroy() if(hive) diff --git a/code/modules/cm_marines/overwatch.dm b/code/modules/cm_marines/overwatch.dm index 6db426a348c3..3cf33ebd391e 100644 --- a/code/modules/cm_marines/overwatch.dm +++ b/code/modules/cm_marines/overwatch.dm @@ -39,8 +39,11 @@ /obj/structure/machinery/computer/overwatch/Initialize() . = ..() - tacmap = new(src, minimap_type) + if (faction == FACTION_MARINE) + tacmap = new /datum/tacmap/drawing(src, minimap_type) + else + tacmap = new(src, minimap_type) // Non-drawing version /obj/structure/machinery/computer/overwatch/Destroy() QDEL_NULL(tacmap) diff --git a/code/modules/cm_preds/yaut_machines.dm b/code/modules/cm_preds/yaut_machines.dm index a1782ca22b85..f076c6782d9a 100644 --- a/code/modules/cm_preds/yaut_machines.dm +++ b/code/modules/cm_preds/yaut_machines.dm @@ -6,6 +6,7 @@ breakable = FALSE minimap_type = MINIMAP_FLAG_ALL + faction = FACTION_YAUTJA /obj/structure/machinery/autolathe/yautja name = "yautja autolathe" diff --git a/code/modules/desert_dam/motion_sensor/sensortower.dm b/code/modules/desert_dam/motion_sensor/sensortower.dm index 5783d0ce9f20..4ef11c32245d 100644 --- a/code/modules/desert_dam/motion_sensor/sensortower.dm +++ b/code/modules/desert_dam/motion_sensor/sensortower.dm @@ -68,7 +68,7 @@ return SSminimaps.remove_marker(current_xeno) - current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|MINIMAP_FLAG_XENO) + current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|get_minimap_flag_for_faction(current_xeno.hivenumber)) minimap_added += WEAKREF(current_xeno) /obj/structure/machinery/sensortower/proc/checkfailure() diff --git a/code/modules/escape_menu/admin_buttons.dm b/code/modules/escape_menu/admin_buttons.dm index e6771d05bf68..661901c1b77a 100644 --- a/code/modules/escape_menu/admin_buttons.dm +++ b/code/modules/escape_menu/admin_buttons.dm @@ -46,7 +46,7 @@ new /atom/movable/screen/escape_menu/home_button( null, src, - "Medal Panel", + "Medals Panel", /* offset = */ 5, CALLBACK(src, PROC_REF(home_medal)), ) @@ -56,8 +56,18 @@ new /atom/movable/screen/escape_menu/home_button( null, src, - "Teleport Panel", + "Tacmaps Panel", /* offset = */ 6, + CALLBACK(src, PROC_REF(home_tacmaps)), + ) + ) + + page_holder.give_screen_object( + new /atom/movable/screen/escape_menu/home_button( + null, + src, + "Teleport Panel", + /* offset = */ 7, CALLBACK(src, PROC_REF(home_teleport)), ) ) @@ -67,7 +77,7 @@ null, src, "Inview Panel", - /* offset = */ 7, + /* offset = */ 8, CALLBACK(src, PROC_REF(home_inview)), ) ) @@ -77,7 +87,7 @@ null, src, "Unban Panel", - /* offset = */ 8, + /* offset = */ 9, CALLBACK(src, PROC_REF(home_unban)), ) ) @@ -87,7 +97,7 @@ null, src, "Shuttle Manipulator", - /* offset = */ 9, + /* offset = */ 10, CALLBACK(src, PROC_REF(home_shuttle)), ) ) @@ -117,6 +127,12 @@ GLOB.medals_panel.tgui_interact(client?.mob) +/datum/escape_menu/proc/home_tacmaps() + if(!client?.admin_holder.check_for_rights(R_ADMIN|R_MOD)) + return + + GLOB.tacmap_admin_panel.tgui_interact(client?.mob) + /datum/escape_menu/proc/home_teleport() if(!client?.admin_holder.check_for_rights(R_MOD)) return diff --git a/code/modules/maptext_alerts/screen_alerts.dm b/code/modules/maptext_alerts/screen_alerts.dm index 820c64301bc2..0b923f7dc753 100644 --- a/code/modules/maptext_alerts/screen_alerts.dm +++ b/code/modules/maptext_alerts/screen_alerts.dm @@ -246,3 +246,8 @@ ghost_user.do_observe(target) if(NOTIFY_JOIN_XENO) ghost_user.join_as_alien() + if(NOTIFY_USCM_TACMAP) + GLOB.uscm_tacmap_status.tgui_interact(ghost_user) + if(NOTIFY_XENO_TACMAP) + GLOB.xeno_tacmap_status.tgui_interact(ghost_user) + diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index da0560e151e9..a68a67cfdf53 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -377,6 +377,10 @@ handle_joining_as_freed_mob(locate(href_list["claim_freed"])) if(href_list["join_xeno"]) join_as_alien() + if(href_list[NOTIFY_USCM_TACMAP]) + GLOB.uscm_tacmap_status.tgui_interact(src) + if(href_list[NOTIFY_XENO_TACMAP]) + GLOB.xeno_tacmap_status.tgui_interact(src) /mob/dead/observer/proc/set_huds_from_prefs() if(!client || !client.prefs) @@ -898,6 +902,23 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp GLOB.hive_datum[hives[faction]].hive_ui.open_hive_status(src) +/mob/dead/observer/verb/view_uscm_tacmap() + set name = "View USCM Tacmap" + set category = "Ghost.View" + + GLOB.uscm_tacmap_status.tgui_interact(src) + +/mob/dead/observer/verb/view_xeno_tacmap() + set name = "View Xeno Tacmap" + set category = "Ghost.View" + + var/datum/hive_status/hive = GLOB.hive_datum[XENO_HIVE_NORMAL] + if(!hive || !length(hive.totalXenos)) + to_chat(src, SPAN_ALERT("There seems to be no living normal hive at the moment")) + return + + GLOB.xeno_tacmap_status.tgui_interact(src) + /mob/dead/verb/join_as_alien() set category = "Ghost.Join" set name = "Join as Xeno" diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 2ec8ccf2531d..3bc8e97623da 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -115,7 +115,9 @@ . += "Primary Objective: [html_decode(assigned_squad.primary_objective)]" if(assigned_squad.secondary_objective) . += "Secondary Objective: [html_decode(assigned_squad.secondary_objective)]" - + if(faction == FACTION_MARINE) + . += "" + . += "View Tactical Map" if(mobility_aura) . += "Active Order: MOVE" if(protection_aura) diff --git a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm index 51cab73e80e6..7beaaab8a04e 100644 --- a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm +++ b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm @@ -508,7 +508,11 @@ if(queen.can_not_harm(src)) return COMPONENT_SCREECH_ACT_CANCEL -/mob/living/carbon/xenomorph/proc/add_minimap_marker(flags = MINIMAP_FLAG_XENO) +/// Adds a minimap marker for this xeno using the provided flags. +/// If flags is 0, it will use get_minimap_flag_for_faction for this xeno +/mob/living/carbon/xenomorph/proc/add_minimap_marker(flags) + if(!flags) + flags = get_minimap_flag_for_faction(hivenumber) if(IS_XENO_LEADER(src)) SSminimaps.add_marker(src, z, hud_flags = flags, given_image = caste.get_minimap_icon(), overlay_iconstates = list(caste.minimap_leadered_overlay)) return diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm index 65ea443c133c..23da1ce65903 100644 --- a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm +++ b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_powers.dm @@ -698,5 +698,5 @@ set name = "View Xeno Tacmap" set desc = "This opens a tactical map, where you can see where every xenomorph is." set category = "Alien" - hive.tacmap.tgui_interact(src) + diff --git a/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm b/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm index f1fff4fb765e..79f73631c7b1 100644 --- a/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm +++ b/code/modules/mob/living/carbon/xenomorph/xeno_defines.dm @@ -362,7 +362,7 @@ /// This number divides the total xenos counted for slots to give the max number of lesser drones var/playable_lesser_drones_max_divisor = 3 - var/datum/tacmap/xeno/tacmap + var/datum/tacmap/drawing/xeno/tacmap var/minimap_type = MINIMAP_FLAG_XENO /datum/hive_status/New() @@ -370,6 +370,7 @@ hive_ui = new(src) mark_ui = new(src) faction_ui = new(src) + minimap_type = get_minimap_flag_for_faction(hivenumber) tacmap = new(src, minimap_type) if(!internal_faction) internal_faction = name diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index a6b654ba2da1..cebe265a673c 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -150,7 +150,7 @@ observer.set_huds_from_prefs() qdel(src) - return 1 + return TRUE if("late_join") @@ -276,11 +276,11 @@ if(player.get_playtime(STATISTIC_HUMAN) == 0 && player.get_playtime(STATISTIC_XENO) == 0) msg_admin_niche("NEW JOIN: [key_name(character, 1, 1, 0)]. IP: [character.lastKnownIP], CID: [character.computer_id]") if(character.client) - var/client/C = character.client - if(C.player_data && C.player_data.playtime_loaded && length(C.player_data.playtimes) == 0) + var/client/client = character.client + if(client.player_data && client.player_data.playtime_loaded && length(client.player_data.playtimes) == 0) msg_admin_niche("NEW PLAYER: [key_name(character, 1, 1, 0)]. IP: [character.lastKnownIP], CID: [character.computer_id]") - if(C.player_data && C.player_data.playtime_loaded && ((round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1)) <= 5)) - msg_sea("NEW PLAYER: [key_name(character, 0, 1, 0)] only has [(round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1))] hours as a human. Current role: [get_actual_job_name(character)] - Current location: [get_area(character)]") + if(client.player_data && client.player_data.playtime_loaded && ((round(client.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1)) <= 5)) + msg_sea("NEW PLAYER: [key_name(character, 0, 1, 0)] only has [(round(client.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1))] hours as a human. Current role: [get_actual_job_name(character)] - Current location: [get_area(character)]") character.client.init_verbs() qdel(src) diff --git a/code/modules/vehicles/apc/apc_command.dm b/code/modules/vehicles/apc/apc_command.dm index c5bd55928362..ace9df2b2a25 100644 --- a/code/modules/vehicles/apc/apc_command.dm +++ b/code/modules/vehicles/apc/apc_command.dm @@ -59,7 +59,7 @@ continue SSminimaps.remove_marker(current_xeno) - current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|MINIMAP_FLAG_XENO) + current_xeno.add_minimap_marker(MINIMAP_FLAG_USCM|get_minimap_flag_for_faction(current_xeno.hivenumber)) minimap_added += WEAKREF(current_xeno) else if(WEAKREF(current_xeno) in minimap_added) diff --git a/colonialmarines.dme b/colonialmarines.dme index 9ef2ad37c605..293b69c60d1c 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -1377,6 +1377,8 @@ #include "code\modules\admin\tabs\event_tab.dm" #include "code\modules\admin\tabs\round_tab.dm" #include "code\modules\admin\tabs\server_tab.dm" +#include "code\modules\admin\tacmap_panel\tacmap_admin_panel.dm" +#include "code\modules\admin\tacmap_panel\tacmap_admin_panel_tgui.dm" #include "code\modules\admin\topic\topic.dm" #include "code\modules\admin\topic\topic_chems.dm" #include "code\modules\admin\topic\topic_events.dm" diff --git a/html/statbrowser.js b/html/statbrowser.js index 105270ad298e..289536d37da1 100644 --- a/html/statbrowser.js +++ b/html/statbrowser.js @@ -374,6 +374,8 @@ function draw_debug() { document.getElementById("statcontent").appendChild(table3); } function draw_status() { + var status_tab_map_href_exception = + "View Tactical Map"; if (!document.getElementById("Status")) { createStatusTab("Status"); current_tab = "Status"; @@ -384,6 +386,13 @@ function draw_status() { document .getElementById("statcontent") .appendChild(document.createElement("br")); + } else if ( + // hardcoded because merely using .includes() to test for a href seems unreliable for some reason. + status_tab_parts[i] == status_tab_map_href_exception + ) { + var maplink = document.createElement("a"); + maplink.innerHTML = status_tab_parts[i]; + document.getElementById("statcontent").appendChild(maplink); } else { var div = document.createElement("div"); div.textContent = status_tab_parts[i]; diff --git a/tgui/packages/tgui/interfaces/CanvasLayer.js b/tgui/packages/tgui/interfaces/CanvasLayer.js new file mode 100644 index 000000000000..e647ae765b1c --- /dev/null +++ b/tgui/packages/tgui/interfaces/CanvasLayer.js @@ -0,0 +1,311 @@ +import { Box, Icon, Tooltip } from '../components'; +import { Component, createRef } from 'inferno'; + +// this file should probably not be in interfaces, should move it later. +export class CanvasLayer extends Component { + constructor(props) { + super(props); + this.canvasRef = createRef(); + + // color selection + // using this.state prevents unpredictable behavior + this.state = { + selection: this.props.selection, + mapLoad: true, + }; + + // needs to be of type png of jpg + this.img = null; + this.imageSrc = this.props.imageSrc; + + // stores the stacked lines + this.lineStack = []; + + // stores the individual line drawn + this.currentLine = []; + + this.ctx = null; + this.isPainting = false; + this.lastX = null; + this.lastY = null; + + this.complexity = 0; + } + + componentDidMount() { + this.ctx = this.canvasRef.current.getContext('2d'); + this.ctx.lineWidth = 4; + this.ctx.lineCap = 'round'; + + this.img = new Image(); + + this.img.src = this.imageSrc; + + this.img.onload = () => { + this.setState({ mapLoad: true }); + }; + + this.img.onerror = () => { + this.setState({ mapLoad: false }); + }; + + this.drawCanvas(); + } + + handleMouseDown = (e) => { + this.isPainting = true; + + const rect = this.canvasRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + this.ctx.beginPath(); + this.ctx.moveTo(this.lastX, this.lastY); + this.lastX = x; + this.lastY = y; + }; + + handleMouseMove = (e) => { + if (!this.isPainting || !this.state.selection) { + return; + } + if (e.buttons === 0) { + // We probably dragged off the window - lets not get stuck drawing + this.handleMouseUp(e); + return; + } + + this.ctx.strokeStyle = this.state.selection; + + const rect = this.canvasRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + if (this.lastX !== null && this.lastY !== null) { + // this controls how often we make new strokes + if (Math.abs(this.lastX - x) + Math.abs(this.lastY - y) < 20) { + return; + } + + this.ctx.moveTo(this.lastX, this.lastY); + this.ctx.lineTo(x, y); + this.ctx.stroke(); + this.currentLine.push([ + this.lastX, + this.lastY, + x, + y, + this.ctx.strokeStyle, + ]); + } + + this.lastX = x; + this.lastY = y; + }; + + handleMouseUp = (e) => { + if ( + this.isPainting && + this.state.selection && + this.lastX !== null && + this.lastY !== null + ) { + const rect = this.canvasRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + this.ctx.moveTo(this.lastX, this.lastY); + this.ctx.lineTo(x, y); + this.ctx.stroke(); + this.currentLine.push([ + this.lastX, + this.lastY, + x, + y, + this.ctx.strokeStyle, + ]); + } + + this.isPainting = false; + this.lastX = null; + this.lastY = null; + + if (this.currentLine.length === 0) { + return; + } + + this.lineStack.push([...this.currentLine]); + this.currentLine = []; + this.complexity = this.getComplexity(); + this.props.onDraw(); + }; + + handleSelectionChange = () => { + const { selection } = this.props; + + if (selection === 'clear') { + this.ctx.clearRect( + 0, + 0, + this.canvasRef.current.width, + this.canvasRef.current.height + ); + this.ctx.drawImage( + this.img, + 0, + 0, + this.canvasRef.current.width, + this.canvasRef.current.height + ); + + this.lineStack = []; + this.complexity = 0; + return; + } + + if (selection === 'undo') { + if (this.lineStack.length === 0) { + return; + } + + const line = this.lineStack.pop(); + if (line.length === 0) { + return; + } + + const prevColor = line[0][4]; + + this.ctx.clearRect( + 0, + 0, + this.canvasRef.current.width, + this.canvasRef.current.height + ); + this.ctx.drawImage( + this.img, + 0, + 0, + this.canvasRef.current.width, + this.canvasRef.current.height + ); + this.ctx.globalCompositeOperation = 'source-over'; + + this.lineStack.forEach((currentLine) => { + currentLine.forEach(([lastX, lastY, x, y, colorSelection]) => { + this.ctx.strokeStyle = colorSelection; + this.ctx.beginPath(); + this.ctx.moveTo(lastX, lastY); + this.ctx.lineTo(x, y); + this.ctx.stroke(); + }); + }); + + this.complexity = this.getComplexity(); + this.setState({ selection: prevColor }); + this.props.onUndo(prevColor); + return; + } + + if (selection === 'export') { + const svgData = this.convertToSVG(); + this.props.onImageExport(svgData); + return; + } + + this.setState({ selection: selection }); + }; + + componentDidUpdate(prevProps) { + if (prevProps.actionQueueChange !== this.props.actionQueueChange) { + this.handleSelectionChange(); + } + } + + drawCanvas() { + this.img.onload = () => { + // this onload may or may not be causing problems. + this.ctx.drawImage( + this.img, + 0, + 0, + this.canvasRef.current?.width, + this.canvasRef.current?.height + ); + }; + } + + convertToSVG() { + const lines = this.lineStack.flat(); + const combinedArray = lines.flatMap( + ([lastX, lastY, x, y, colorSelection]) => [ + lastX, + lastY, + x, + y, + colorSelection, + ] + ); + return combinedArray; + } + + getComplexity() { + let count = 0; + this.lineStack.forEach((item) => { + count += item.length; + }); + return count; + } + + displayCanvas() { + return ( +
+ {this.complexity > 500 && ( + + + + )} + this.handleMouseDown(e)} + onMouseUp={(e) => this.handleMouseUp(e)} + onMouseMove={(e) => this.handleMouseMove(e)} + /> +
+ ); + } + + displayLoading() { + return ( +
+ +

+ Please wait a few minutes before attempting to access the canvas. +

+
+
+ ); + } + + render() { + if (this.state.mapLoad) { + return this.displayCanvas(); + } else { + // edge case where a new user joins and tries to draw on the canvas before they cached the png + return this.displayLoading(); + } + } +} diff --git a/tgui/packages/tgui/interfaces/DrawnMap.js b/tgui/packages/tgui/interfaces/DrawnMap.js new file mode 100644 index 000000000000..cd5a9539f847 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DrawnMap.js @@ -0,0 +1,110 @@ +import { Box } from '../components'; +import { Component, createRef } from 'inferno'; + +export class DrawnMap extends Component { + constructor(props) { + super(props); + this.containerRef = createRef(); + this.flatImgSrc = this.props.flatImage; + this.backupImgSrc = this.props.backupImage; + this.state = { + mapLoad: true, + loadingBackup: true, + }; + this.img = null; + this.svg = this.props.svgData; + } + + componentDidMount() { + this.img = new Image(); + this.img.src = this.flatImgSrc; + this.img.onload = () => { + this.setState({ mapLoad: true }); + }; + + this.img.onerror = () => { + this.img.src = this.backupImgSrc; + this.setState({ mapLoad: false }); + }; + + const backupImg = new Image(); + backupImg.src = this.backupImgSrc; + backupImg.onload = () => { + this.setState({ loadingBackup: false }); + }; + } + + parseSvgData(svgDataArray) { + if (!svgDataArray) return null; + let lines = []; + for (let i = 0; i < svgDataArray.length; i += 5) { + const x1 = svgDataArray[i]; + const y1 = svgDataArray[i + 1]; + const x2 = svgDataArray[i + 2]; + const y2 = svgDataArray[i + 3]; + const stroke = svgDataArray[i + 4]; + lines.push({ x1, y1, x2, y2, stroke }); + } + return lines; + } + + getSize() { + const ratio = Math.min( + (self.innerWidth - 50) / 650, + (self.innerHeight - 150) / 600 + ); + return { width: 650 * ratio, height: 600 * ratio }; + } + + render() { + const parsedSvgData = this.parseSvgData(this.svg); + const size = this.getSize(); + + return ( +
+ {this.state.loadingBackup && !this.state.mapLoad && ( + +

Loading map...

+
+ )} + {this.img && this.state.mapLoad && ( + + )} + {parsedSvgData && this.state.mapLoad && ( + + {parsedSvgData.map((line, index) => ( + + ))} + + )} +
+ ); + } +} diff --git a/tgui/packages/tgui/interfaces/OverwatchConsole.js b/tgui/packages/tgui/interfaces/OverwatchConsole.js index 1a6f67ac7ccd..1805f231fb16 100644 --- a/tgui/packages/tgui/interfaces/OverwatchConsole.js +++ b/tgui/packages/tgui/interfaces/OverwatchConsole.js @@ -61,7 +61,6 @@ const SquadPanel = (props, context) => { const { act, data } = useBackend(context); const [category, setCategory] = useLocalState(context, 'selected', 'monitor'); - let hello = 2; return ( <> diff --git a/tgui/packages/tgui/interfaces/TacmapAdminPanel.js b/tgui/packages/tgui/interfaces/TacmapAdminPanel.js new file mode 100644 index 000000000000..a5d00c688a2f --- /dev/null +++ b/tgui/packages/tgui/interfaces/TacmapAdminPanel.js @@ -0,0 +1,164 @@ +import { useBackend, useLocalState } from '../backend'; +import { Tabs, Section, Button, Stack, Flex } from '../components'; +import { DrawnMap } from './DrawnMap'; +import { Window } from '../layouts'; + +const PAGES = [ + { + title: 'USCM', + component: () => FactionPage, + color: 'blue', + icon: 'medal', + }, + { + title: 'Hive', + component: () => FactionPage, + color: 'purple', + icon: 'star', + }, +]; + +export const TacmapAdminPanel = (props, context) => { + const { data } = useBackend(context); + const { + uscm_map, + xeno_map, + uscm_svg, + xeno_svg, + uscm_ckeys, + xeno_ckeys, + uscm_names, + xeno_names, + uscm_times, + xeno_times, + uscm_selection, + xeno_selection, + map_fallback, + last_update_time, + } = data; + + const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0); + + const PageComponent = PAGES[pageIndex].component(); + + return ( + + + + + + {PAGES.map((page, i) => { + if (page.canAccess && !page.canAccess(data)) { + return; + } + + return ( + setPageIndex(i)}> + {page.title} + + ); + })} + + + + + + +
+ +
+
+
+
+
+ ); +}; + +const FactionPage = (props, context) => { + const { act } = useBackend(context); + const { svg, ckeys, names, times, selected_map, is_uscm } = props; + + return ( +
+ act('recache', { + uscm: is_uscm, + }) + } + /> + }> + {Object(ckeys).map((ckey, ckey_index) => ( + + + + act('change_selection', { + uscm: is_uscm, + index: ckey_index, + }) + } + /> + + + {names[ckey_index]} ({ckey}) - {times[ckey_index]} + + + + act('delete', { + uscm: is_uscm, + index: ckey_index, + }) + } + /> + + + ))} +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/TacticalMap.tsx b/tgui/packages/tgui/interfaces/TacticalMap.tsx index 01ba483acf27..92996038719f 100644 --- a/tgui/packages/tgui/interfaces/TacticalMap.tsx +++ b/tgui/packages/tgui/interfaces/TacticalMap.tsx @@ -1,24 +1,330 @@ -import { useBackend } from '../backend'; -import { ByondUi } from '../components'; +import { useBackend, useLocalState } from '../backend'; +import { Button, Dropdown, Section, Stack, ProgressBar, Box, Tabs } from '../components'; import { Window } from '../layouts'; +import { CanvasLayer } from './CanvasLayer'; +import { DrawnMap } from './DrawnMap'; +import { ByondUi } from '../components'; interface TacMapProps { + toolbarColorSelection: string; + toolbarUpdatedSelection: string; + updatedCanvas: boolean; + themeId: number; + svgData: any; + canViewTacmap: number; + canDraw: number; + isXeno: boolean; + canViewCanvas: number; + newCanvasFlatImage: string; + oldCanvasFlatImage: string; + actionQueueChange: number; + exportedColor: string; + mapFallback: string; mapRef: string; + currentMenu: string; + lastUpdateTime: any; + nextCanvasTime: any; + canvasCooldown: any; + exportedTacMapImage: any; + tacmapReady: number; } +const PAGES = [ + { + title: 'tacmap', + canOpen: (data) => { + return 1; + }, + component: () => ViewMapPanel, + icon: 'map', + canAccess: (data) => { + return data.canViewTacmap; + }, + }, + { + title: 'old canvas', + canOpen: (data) => { + return 1; + }, + component: () => OldMapPanel, + icon: 'eye', + canAccess: (data) => { + return data.canViewCanvas; + }, + }, + { + title: 'new canvas', + canOpen: (data) => { + return data.tacmapReady; + }, + component: () => DrawMapPanel, + icon: 'paintbrush', + canAccess: (data) => { + return data.canDraw; + }, + }, +]; + +const colorOptions = [ + 'black', + 'red', + 'orange', + 'blue', + 'purple', + 'green', + 'brown', +]; + +const colors: Record = { + 'black': '#000000', + 'red': '#fc0000', + 'orange': '#f59a07', + 'blue': '#0561f5', + 'purple': '#c002fa', + 'green': '#02c245', + 'brown': '#5c351e', +}; + export const TacticalMap = (props, context) => { const { data, act } = useBackend(context); + const [pageIndex, setPageIndex] = useLocalState( + context, + 'pageIndex', + data.canViewTacmap ? 0 : 1 + ); + const PageComponent = PAGES[pageIndex].component(); + + const handleTacmapOnClick = (i, pageTitle) => { + setPageIndex(i); + act('menuSelect', { + selection: pageTitle, + }); + }; + return ( - + - +
+ + + + {PAGES.map((page, i) => { + if (page.canAccess(data) === 0) { + return; + } + return ( + handleTacmapOnClick(i, page.title)}> + {page.canOpen(data) === 0 ? 'loading' : page.title} + + ); + })} + + + +
+
); }; + +const ViewMapPanel = (props, context) => { + const { data } = useBackend(context); + + // byond ui can't resist trying to render + if (data.canViewTacmap === 0 || data.mapRef === null) { + return ; + } + + return ( +
+ +
+ ); +}; + +const OldMapPanel = (props, context) => { + const { data } = useBackend(context); + return ( +
+ {data.canViewCanvas ? ( + + ) : ( + +

Unauthorized.

+
+ )} +
+ ); +}; + +const DrawMapPanel = (props, context) => { + const { data, act } = useBackend(context); + + const timeLeftPct = data.canvasCooldown / data.nextCanvasTime; + const canUpdate = data.canvasCooldown <= 0 && !data.updatedCanvas; + + const handleTacMapExport = (image: any) => { + data.exportedTacMapImage = image; + }; + + const handleColorSelection = (dataSelection) => { + if (colors[dataSelection] !== null && colors[dataSelection] !== undefined) { + return colors[dataSelection]; + } else { + return dataSelection; + } + }; + const findColorValue = (oldValue: string) => { + return (Object.keys(colors) as Array).find( + (key) => colors[key] === (oldValue as string) + ); + }; + + return ( + <> +
+ + + {(!data.updatedCanvas && ( +
+
+ + act('selectColor', { color: findColorValue(value) }) + } + onDraw={() => act('onDraw')} + /> +
+ + ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/TacticalMap.scss b/tgui/packages/tgui/styles/interfaces/TacticalMap.scss index 312c9ce262ff..a4ab13451772 100644 --- a/tgui/packages/tgui/styles/interfaces/TacticalMap.scss +++ b/tgui/packages/tgui/styles/interfaces/TacticalMap.scss @@ -9,3 +9,7 @@ margin: 0.5em; text-align: center; } + +.progress-stack { + margin-top: 15px; +} From a4284b4153c9e4c120ac0b4678151a1cb401d56d Mon Sep 17 00:00:00 2001 From: cm13-github <128137806+cm13-github@users.noreply.github.com> Date: Fri, 17 Nov 2023 23:06:32 +0000 Subject: [PATCH 2/2] Automatic changelog for PR #4475 [ci skip] --- html/changelogs/AutoChangeLog-pr-4475.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-4475.yml diff --git a/html/changelogs/AutoChangeLog-pr-4475.yml b/html/changelogs/AutoChangeLog-pr-4475.yml new file mode 100644 index 000000000000..27d43d1e7aca --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-4475.yml @@ -0,0 +1,5 @@ +author: "Cthulhu80, Drathek" +delete-after: True +changes: + - rscadd: "Adds drawing to tactical maps, viewable via stat panel for marines and xeno tacmap for xenos." + - bugfix: "Corrupted (and other hives) now have separate tactical maps." \ No newline at end of file