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 08fcf5d47915..97243002740d 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -681,8 +681,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 @@ -713,6 +714,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 "" @@ -755,6 +758,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 bac97d1994a3..6e1b229e562f 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 65f68d014585..0b7a4af0f05f 100644 --- a/code/_globalvars/misc.dm +++ b/code/_globalvars/misc.dm @@ -100,6 +100,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_EMPTY(gamemode_roles) 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 c15d7f008366..72fc917d81e1 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 530b1fe6593d..7a51539b8888 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 1a15606335e9..7b4c2d5df771 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 2c3171833adf..3f87b2bfbea2 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 60d418d969ba..5cd5784cfe2e 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -69,6 +69,7 @@ GLOBAL_LIST_INIT(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, )) GLOBAL_LIST_INIT(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 42aac0e6c0f6..74ce9a81eb88 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 ec1b48989fc4..e1dbd1f026c1 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 9686b7253c00..6ac90338045d 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 6f571eeddaf1..8e3150cef1df 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 b6bc636c2375..cf3f9e8b4702 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 56765f8a6ef1..66892025ff6a 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 93f1f0da7d20..cf3be6de9086 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 b489a6382898..65769eac9cdf 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 7e24ac6e5f9a..810c8f58666b 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 c53fe47257d2..5938f7f51a2d 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 f39780ff3426..e0862ae4f2ab 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 6d079582773c..652878d97895 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -1375,6 +1375,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/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 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 ( +