diff --git a/code/_onclick/hud/ai.dm b/code/_onclick/hud/ai.dm index f8d0610d410..16f91ef2ab3 100644 --- a/code/_onclick/hud/ai.dm +++ b/code/_onclick/hud/ai.dm @@ -41,7 +41,7 @@ /obj/screen/ai/crew_monitor/Click() var/mob/living/silicon/ai/AI = usr - crewmonitor(AI,AI) + crewmonitor.show(AI) /obj/screen/ai/crew_manifest name = "Crew Manifest" diff --git a/code/controllers/subsystem/procqueue.dm b/code/controllers/subsystem/procqueue.dm new file mode 100644 index 00000000000..daf762927c9 --- /dev/null +++ b/code/controllers/subsystem/procqueue.dm @@ -0,0 +1,46 @@ +var/datum/subsystem/procqueue/procqueue + +/datum/subsystem/procqueue + var/list/queue = list() + +/datum/subsystem/procqueue/New() + NEW_SS_GLOBAL(procqueue) + +/datum/subsystem/procqueue/fire() + if (queue.len) + var/list/L = list() + var/key + + for (var/datum/procqueue_item/item in queue) + key = "[item.ref]_[item.procname]" + + if (item.args) + key += "(" + var/first = 1 + for (var/a in item.args) + if (!first) key += "," + key += "[a]" + first = 0 + key += ")" + + if (!(key in L)) + if (item.args) call(item.ref, item.procname)(arglist(item.args)) + else call(item.ref, item.procname)() + + L.Add(key) + + queue.Cut() + +/datum/subsystem/procqueue/proc/queue(ref, procname, ...) + var/datum/procqueue_item/item = new/datum/procqueue_item + item.ref = ref + item.procname = procname + + if (args.len > 2) + item.args = args.Copy(3) + + src.queue.Insert(1, item) + +/datum/procqueue_item/var/ref +/datum/procqueue_item/var/procname +/datum/procqueue_item/var/list/args \ No newline at end of file diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index fa6db81352b..a8653369af1 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -66,6 +66,7 @@ var/datum/subsystem/ticker/ticker timeLeft = config.lobby_countdown * 10 world << "Welcome to the pre-game lobby!" world << "Please, setup your character and select ready. Game will start in [config.lobby_countdown] seconds" + crewmonitor.generateMiniMaps() // start generating minimaps (this is a background process) current_state = GAME_STATE_PREGAME if(GAME_STATE_PREGAME) @@ -200,6 +201,10 @@ var/datum/subsystem/ticker/ticker //Plus it provides an easy way to make cinematics for other events. Just use this as a template /datum/subsystem/ticker/proc/station_explosion_cinematic(var/station_missed=0, var/override = null) if( cinematic ) return //already a cinematic in progress! + + for (var/datum/html_interface/hi in html_interfaces) + hi.closeAll() + auto_toggle_ooc(1) // Turn it on //initialise our cinematic screen object cinematic = new /obj/screen{icon='icons/effects/station_explosion.dmi';icon_state="station_intact";layer=20;mouse_opacity=0;screen_loc="1,0";}(src) diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm index 688ae0c3b3f..edd4d2ad81b 100644 --- a/code/game/data_huds.dm +++ b/code/game/data_huds.dm @@ -93,6 +93,9 @@ var/datum/atom_hud/data/medical/basic/B = huds[DATA_HUD_MEDICAL_BASIC] B.update_suit_sensors(src) + var/turf/T = get_turf(src) + if (T) crewmonitor.queueUpdate(T.z) + //called when a carbon changes health /mob/living/carbon/proc/med_hud_set_health() var/image/holder = hud_list[HEALTH_HUD] @@ -101,6 +104,9 @@ else holder.icon_state = "hud[RoundHealth(health)]" + var/turf/T = get_turf(src) + if (T) crewmonitor.queueUpdate(T.z) + //called when a carbon changes stat, virus or XENO_HOST /mob/living/carbon/proc/med_hud_set_status() var/image/holder = hud_list[STATUS_HUD] @@ -126,6 +132,9 @@ if(wear_id) holder.icon_state = "hud[ckey(wear_id.GetJobName())]" + var/turf/T = get_turf(src) + if (T) crewmonitor.queueUpdate(T.z) + /mob/living/carbon/human/proc/sec_hud_set_implants() var/image/holder for(var/i in list(IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD)) diff --git a/code/game/machinery/computer/crew.css b/code/game/machinery/computer/crew.css new file mode 100644 index 00000000000..e4f780b6b6f --- /dev/null +++ b/code/game/machinery/computer/crew.css @@ -0,0 +1,105 @@ +body +{ + padding-left: 490px; + cursor: default; +} + +#ntbgcenter +{ + background-position: 550px 0px !important; +} + +#minimap +{ + position: fixed; + top: 8px; + left: 8px; + border: 2px inset #888; +} + +#textbased +{ + width: 100%; +} + +#textbased table +{ + min-width: 380px; + width: 100%; + table-layout: fixed; +} + +#textbased td +{ + vertical-align: top; + padding: 2px; +} + +#textbased tbody td +{ + transition: .2s all; +} + +#textbased tbody tr:hover td, #textbased tbody tr.hover td +{ + background-color: #515151; +} + +#textarea:after +{ + content: ""; + clear: both; +} + +a { display: none; } + +.health +{ + width: 16px; + background-color: #FFF; + border: 1px solid #434343; + position: relative; + top: 2px; +} + +.health-5 { background-color: #17d568; } +.health-4 { background-color: #2ecc71; } +.health-3 { background-color: #e67e22; } +.health-2 { background-color: #ed5100; } +.health-1 { background-color: #e74c3c; } +.health-0 { background-color: #ed2814; } + +.tt +{ + position: relative; + display: inline-block; + height: 16px; +} + +.tt > div +{ + display: none; +} + +.tt:hover > div +{ + position: absolute; + bottom: -30px; + left: 50%; + margin-left: -64px; + + display: block; + width: 128px; + height: 24px; + border: 1px solid #313131; + background-color: #434343; + padding: 4px; + z-index: 999; + text-align: center; +} + +.tt > div > span +{ + position: relative; + top: -2px; +} \ No newline at end of file diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm index 32b7eab5df5..fea01927ad8 100644 --- a/code/game/machinery/computer/crew.dm +++ b/code/game/machinery/computer/crew.dm @@ -11,31 +11,29 @@ /obj/machinery/computer/crew/attack_ai(mob/user) if(stat & (BROKEN|NOPOWER)) return - crewmonitor(user,src) + crewmonitor.show(user) /obj/machinery/computer/crew/attack_hand(mob/user) if(..()) return if(stat & (BROKEN|NOPOWER)) return - crewmonitor(user,src) + crewmonitor.show(user) -/obj/machinery/computer/crew/Topic(href, href_list) - if(..()) return - if (src.z > 6) - usr << "Unable to establish a connection: \black You're too far away from the station!" - return - if( href_list["close"] ) - usr << browse(null, "window=crewcomp") - usr.unset_machine() - return - if(href_list["update"]) - src.updateDialog() - return +var/global/datum/crewmonitor/crewmonitor = new +/datum/crewmonitor + var/list/jobs + var/list/interfaces + var/list/data + var/const/MAX_ICON_DIMENSION = 1024 + var/const/ICON_SIZE = 4 + var/initialized = FALSE -/proc/crewmonitor(mob/user,var/atom/source) - var/jobs[0] +/datum/crewmonitor/New() + . = ..() + + var/list/jobs = new/list() jobs["Captain"] = 00 jobs["Head of Personnel"] = 50 jobs["Head of Security"] = 10 @@ -74,71 +72,162 @@ jobs["Security Response Officer"] = 221 jobs["Engineer Response Officer"] = 222 jobs["Medical Response Officer"] = 223 - jobs["Assistant"] = 999 //Unknowns/custom jobs should appear after civilians, and before assistants + jobs["Assistant"] = 999 //Unknowns/custom jobs should appear after civilians, and before assistants - var/t = "" - var/list/logs = list() - var/list/tracked = crewscan() - var/turf/srcturf = get_turf(source) - for(var/mob/living/carbon/human/H in tracked) - var/log = "" - var/turf/pos = get_turf(H) - if(istype(H.w_uniform, /obj/item/clothing/under)) - var/obj/item/clothing/under/U = H.w_uniform - if(pos && pos.z == srcturf.z && U.sensor_mode) - var/obj/item/weapon/card/id/I = null - if(H.wear_id) - I = H.wear_id.GetID() + src.jobs = jobs + src.interfaces = list() + src.data = list() - var/life_status = "[H.stat > 1 ? "Deceased" : "Living"]" +/datum/crewmonitor/Destroy() + if (src.interfaces) + for (var/datum/html_interface/hi in interfaces) + qdel(hi) + src.interfaces = null - if(I) - var/style = null - var/ijob = jobs[I.assignment] - if(ijob % 10 == 0) - style += "font-weight: bold; " //head roles always end in 0 - if(ijob >= 10 && ijob < 20) - style += "color: #E74C3C; " //security - if(ijob >= 20 && ijob < 30) - style += "color: #3498DB; " //medical - if(ijob >= 30 && ijob < 40) - style += "color: #9B59B6; " //science - if(ijob >= 40 && ijob < 50) - style += "color: #F1C40F; " //engineering - if(ijob >= 50 && ijob < 60) - style += "color: #F39C12; " //cargo - if(ijob >= 200 && ijob < 230) - style += "color: #00C100; " //Centcom - log += "[ijob]]" //ijob does not get displayed, nor does it take up space, it's just used for the positioning of an entry - else - log += "80" + return ..() - var/damage_report - if(U.sensor_mode > 1) - var/dam1 = round(H.getOxyLoss(),1) - var/dam2 = round(H.getToxLoss(),1) - var/dam3 = round(H.getFireLoss(),1) - var/dam4 = round(H.getBruteLoss(),1) - damage_report = "([dam1]/[dam2]/[dam3]/[dam4])" +/datum/crewmonitor/proc/show(mob/mob, z) + if (!z) z = mob.z - switch(U.sensor_mode) - if(1) - log += "" - if(2) - log += "" - if(3) - var/area/player_area = get_area(H) - log += "" - logs += log - logs = sortList(logs) - for(var/log in logs) - t += log - t += "

Name

Vitals

Position

[I.registered_name] ([I.assignment])
Unknown[life_status]Not Available
[life_status] [damage_report]Not Available
[life_status] [damage_report][format_text(player_area.name)] ([pos.x], [pos.y])
" - var/datum/browser/popup = new(user, "crewcomp", "Crew Monitoring", 900, 600) - popup.set_content(t) - popup.open() + if (z > 0 && src.interfaces) + var/datum/html_interface/hi + if (!src.interfaces["[z]"]) + src.interfaces["[z]"] = new/datum/html_interface/nanotrasen(src, "Crew Monitoring", 900, 540, "") + hi = src.interfaces["[z]"] + + hi.updateContent("content", "Switch to mini map Switch to text-based
") + + src.update(z, TRUE) + else + hi = src.interfaces["[z]"] + + hi = src.interfaces["[z]"] + hi.show(mob) + src.updateFor(mob, hi, z) + +/datum/crewmonitor/proc/updateFor(hclient_or_mob, datum/html_interface/hi, z) + // This check will succeed if updateFor is called after showing to the player, but will fail + // on regular updates. Since we only really need this once we don't care if it fails. + var/list/parameters = list(ismob(hclient_or_mob) && isAI(hclient_or_mob) ? "true" : "false") + + hi.callJavaScript("clearAll", parameters, hclient_or_mob) + + for (var/list/L in data) + hi.callJavaScript("add", L, hclient_or_mob) + +/datum/crewmonitor/proc/update(z, ignore_unused = FALSE) + if (src.interfaces["[z]"]) + var/datum/html_interface/hi = src.interfaces["[z]"] + + if (ignore_unused || hi.isUsed()) + var/list/results = list() + var/obj/item/clothing/under/U + var/obj/item/weapon/card/id/I + var/turf/pos + var/ijob + var/name + var/assignment + var/dam1 + var/dam2 + var/dam3 + var/dam4 + var/area + var/pos_x + var/pos_y + var/life_status + + for(var/mob/living/carbon/human/H in mob_list) + // Check if their z-level is correct and if they are wearing a uniform. + // Accept H.z==0 as well in case the mob is inside an object. + if ((H.z == 0 || H.z == z) && istype(H.w_uniform, /obj/item/clothing/under)) + U = H.w_uniform + + // Are the suit sensors on? + if (U.has_sensor && U.sensor_mode) + pos = H.z == 0 || U.sensor_mode == 3 ? get_turf(H) : null + + // Special case: If the mob is inside an object confirm the z-level on turf level. + if (H.z == 0 && (!pos || pos.z != z)) continue + + I = H.wear_id ? H.wear_id.GetID() : null + + if (I) + name = I.registered_name + assignment = I.assignment + ijob = jobs[I.assignment] + else + name = "Unknown" + assignment = "" + ijob = 80 + + if (U.sensor_mode >= 1) life_status = (!H.stat ? "true" : "false") + else life_status = null + + if (U.sensor_mode >= 2) + dam1 = round(H.getOxyLoss(),1) + dam2 = round(H.getToxLoss(),1) + dam3 = round(H.getFireLoss(),1) + dam4 = round(H.getBruteLoss(),1) + else + dam1 = null + dam2 = null + dam3 = null + dam4 = null + + if (U.sensor_mode >= 3) + if (!pos) pos = get_turf(H) + var/area/player_area = get_area(H) + + area = format_text(player_area.name) + pos_x = pos.x + pos_y = pos.y + else + area = null + pos_x = null + pos_y = null + + results[++results.len] = list(name, assignment, ijob, life_status, dam1, dam2, dam3, dam4, area, pos_x, pos_y, H.can_track(null)) + + src.data = results + src.updateFor(null, hi, z) // updates for everyone + +/datum/crewmonitor/proc/hiIsValidClient(datum/html_interface_client/hclient, datum/html_interface/hi) + var/z = "" + + for (z in src.interfaces) + if (src.interfaces[z] == hi) break + + return (hclient.client.mob && hclient.client.mob.stat == 0 && hclient.client.mob.z == text2num(z) && ( isAI(hclient.client.mob) || (locate(/obj/machinery/computer/crew, range(1, hclient.client.mob))) || (locate(/obj/item/device/sensor_device, hclient.client.mob.contents)) ) ) + +/datum/crewmonitor/Topic(href, href_list[], datum/html_interface_client/hclient) + if (istype(hclient)) + if (hclient && hclient.client && hclient.client.mob && isAI(hclient.client.mob)) + var/mob/living/silicon/ai/AI = hclient.client.mob + + switch (href_list["action"]) + if ("select_person") + AI.ai_camera_track(href_list["name"]) + + if ("select_position") + var/x = text2num(href_list["x"]) + var/y = text2num(href_list["y"]) + var/turf/tile = locate(x, y, AI.z) + + var/obj/machinery/camera/C = locate(/obj/machinery/camera) in range(5, tile) + + if (!C) C = locate(/obj/machinery/camera) in range(10, tile) + if (!C) C = locate(/obj/machinery/camera) in range(15, tile) + + if (C) + var/turf/current_loc = AI.eyeobj.loc + + spawn(min(30, get_dist(get_turf(C), AI.eyeobj) / 4)) + if (AI && AI.eyeobj && current_loc == AI.eyeobj.loc) + AI.switchCamera(C) + +/* /proc/crewscan() var/list/tracked = list() for(var/mob/living/carbon/human/H in mob_list) @@ -146,4 +235,196 @@ var/obj/item/clothing/under/U = H.w_uniform if(U.has_sensor && U.sensor_mode) tracked.Add(H) - return tracked \ No newline at end of file + return tracked +*/ + +/mob/living/carbon/human/Move() + if (src.w_uniform) + var/old_z = src.z + + . = ..() + + if (old_z != src.z) crewmonitor.queueUpdate(old_z) + crewmonitor.queueUpdate(src.z) + else + return ..() + +/datum/crewmonitor/proc/queueUpdate(z) + procqueue.queue(crewmonitor, "update", z) + +/datum/crewmonitor/proc/generateMiniMaps() + spawn + for (var/z = 1 to world.maxz) src.generateMiniMap(z) + + NOTICE("MINIMAP: All minimaps have been generated.") + + for (var/client/C in clients) + src.sendResources(C) + + src.initialized = TRUE + +/datum/crewmonitor/proc/sendResources(client/C) + C << browse_rsc('crew.js', "crewmonitor.js") + C << browse_rsc('crew.css', "crewmonitor.css") + for (var/z = 1 to world.maxz) C << browse_rsc(file("[getMinimapFile(z)].png"), "minimap_[z].png") + +/datum/crewmonitor/proc/getMinimapFile(z) + return "data/minimaps/map_[z]" + +// Activate this to debug tile mismatches in the minimap. +// This will store the full information on each tile and compare it the next time you run the minimap. +// It can be used to find out what's changed since the last iteration. +// Only activate this when you need it - this should never be active on a live server! +// #define MINIMAP_DEBUG + +/datum/crewmonitor/proc/generateMiniMap(z, x1 = 1, y1 = 1, x2 = world.maxx, y2 = world.maxy) + var/result_path = "[src.getMinimapFile(z)].png" + var/hash_path = "[src.getMinimapFile(z)].md5" + var/list/tiles = block(locate(x1, y1, z), locate(x2, y2, z)) + var/hash = "" + var/temp + var/obj/obj + + #ifdef MINIMAP_DEBUG + var/tiledata_path = "data/minimaps/debug_tiledata_[z].sav" + var/savefile/F = new/savefile(tiledata_path) + #endif + + // Note for future developer: If you have tiles on the map with random or dynamic icons this hash check will fail + // every time. You'll have to modify this code to generate a unique hash for your object. + // Don't forget to modify the minimap generation code to use a default icon (or skip generation altogether). + for (var/turf/tile in tiles) + if (istype(tile.loc, /area/asteroid) || istype(tile.loc, /area/mine/unexplored) || istype(tile, /turf/simulated/mineral) || (istype(tile.loc, /area/space) && istype(tile, /turf/simulated/floor/plating/asteroid))) + temp = "/area/asteroid" + else if (istype(tile.loc, /area/mine) && istype(tile, /turf/simulated/floor/plating/asteroid)) + temp = "/area/mine/explored" + else if (tile.loc.type == /area/start || (tile.type == /turf/space && !(locate(/obj/structure/lattice) in tile)) || istype(tile, /turf/space/transit)) + temp = "/turf/space" + if (locate(/obj/structure/lattice/catwalk) in tile) + + else + else if (tile.type == /turf/space) + if (locate(/obj/structure/lattice/catwalk) in tile) + temp = "/obj/structure/lattice/catwalk" + else + temp = "/obj/structure/lattice" + else if (tile.type == /turf/simulated/floor/plating/abductor) + temp = "/turf/simulated/floor/plating/abductor" + else if (tile.type == /turf/simulated/floor/plating && (locate(/obj/structure/window/shuttle) in tile)) + temp = "/obj/structure/window/shuttle" + else + temp = "[tile.icon][tile.icon_state][tile.dir]" + + obj = locate(/obj/structure/transit_tube) in tile + + if (obj) temp = "[temp]/obj/structure/transit_tube[obj.icon_state][obj.dir]" + + #ifdef MINIMAP_DEBUG + if (F["/[tile.y]/[tile.x]"] && F["/[tile.y]/[tile.x]"] != temp) + CRASH("Mismatch: [tile.type] at [tile.x],[tile.y],[tile.z] ([tile.icon], [tile.icon_state], [tile.dir])") + else + F["/[tile.y]/[tile.x]"] << temp + #endif + + hash = md5("[hash][temp]") + + if (fexists(result_path)) + if (!fexists(hash_path) || trim(file2text(hash_path)) != hash) + fdel(result_path) + fdel(hash_path) + + if (!fexists(result_path)) + ASSERT(x1 > 0) + ASSERT(y1 > 0) + ASSERT(x2 <= world.maxx) + ASSERT(y2 <= world.maxy) + + var/icon/map_icon = new/icon('html/mapbase1024.png') + + // map_icon is fine and contains only 1 direction at this point. + + ASSERT(map_icon.Width() == MAX_ICON_DIMENSION && map_icon.Height() == MAX_ICON_DIMENSION) + + NOTICE("MINIMAP: Generating minimap for z-level [z].") + + var/i = 0 + var/icon/turf_icon + var/icon/obj_icon + var/old_icon + var/old_icon_state + var/old_dir + var/new_icon + var/new_icon_state + var/new_dir + + for (var/turf/tile in tiles) + if (tile.loc.type != /area/start && (tile.type != /turf/space || (locate(/obj/structure/lattice) in tile) || (locate(/obj/structure/transit_tube) in tile)) && !istype(tile, /turf/space/transit)) + if (istype(tile.loc, /area/asteroid) || istype(tile.loc, /area/mine/unexplored) || istype(tile, /turf/simulated/mineral) || (istype(tile.loc, /area/space) && istype(tile, /turf/simulated/floor/plating/asteroid))) + new_icon = 'icons/turf/mining.dmi' + new_icon_state = "rock" + new_dir = 2 + else if (istype(tile.loc, /area/mine) && istype(tile, /turf/simulated/floor/plating/asteroid)) + new_icon = 'icons/turf/floors.dmi' + new_icon_state = "asteroid" + new_dir = 2 + else if (tile.type == /turf/simulated/floor/plating/abductor) + new_icon = 'icons/turf/floors.dmi' + new_icon_state = "alienpod1" + new_dir = 2 + else if (tile.type == /turf/space) + obj = locate(/obj/structure/lattice) in tile + + if (!obj) obj = locate(/obj/structure/transit_tube) in tile + + ASSERT(obj != null) + + if (obj) + new_icon = obj.icon + new_dir = obj.dir + new_icon_state = obj.icon_state + else if (tile.type == /turf/simulated/floor/plating && (locate(/obj/structure/window/shuttle) in tile)) + new_icon = 'icons/obj/structures.dmi' + new_dir = 2 + new_icon_state = "swindow" + else + new_icon = tile.icon + new_icon_state = tile.icon_state + new_dir = tile.dir + + if (new_icon != old_icon || new_icon_state != old_icon_state || new_dir != old_dir) + old_icon = new_icon + old_icon_state = new_icon_state + old_dir = new_dir + + turf_icon = new/icon(new_icon, new_icon_state, new_dir, 1, 0) + turf_icon.Scale(ICON_SIZE, ICON_SIZE) + + if (tile.type != /turf/space || (locate(/obj/structure/lattice) in tile)) + obj = locate(/obj/structure/transit_tube) in tile + + if (obj) + obj_icon = new/icon(obj.icon, obj.icon_state, obj.dir, 1, 0) + obj_icon.Scale(ICON_SIZE, ICON_SIZE) + turf_icon.Blend(obj_icon, ICON_OVERLAY) + + map_icon.Blend(turf_icon, ICON_OVERLAY, ((tile.x - 1) * ICON_SIZE), ((tile.y - 1) * ICON_SIZE)) + + if ((++i) % 512 == 0) sleep(1) // deliberate delay to avoid lag spikes + + if ((i % 1024) == 0) NOTICE("MINIMAP: Generated [i] of [tiles.len] tiles.") + else + sleep(-1) // avoid sleeping if possible: prioritize pending procs + + NOTICE("MINIMAP: Generated [tiles.len] of [tiles.len] tiles.") + + // BYOND BUG: map_icon now contains 4 directions? Create a new icon with only a single state. + var/icon/result_icon = new/icon() + + result_icon.Insert(map_icon, "", SOUTH, 1, 0) + + fcopy(result_icon, result_path) + text2file(hash, hash_path) + +#ifdef MINIMAP_DEBUG +#undef MINIMAP_DEBUG +#endif diff --git a/code/game/machinery/computer/crew.js b/code/game/machinery/computer/crew.js new file mode 100644 index 00000000000..d81d59447d9 --- /dev/null +++ b/code/game/machinery/computer/crew.js @@ -0,0 +1,196 @@ +var isAI = null; +var scale_x; +var scale_y; + +function disableSelection(){ return false; }; + +$(window).on("onUpdateContent", function() +{ + $("#textbased").html("

Name

 

Position

"); + + $("#minimap").append(""); + + $("body")[0].onselectstart = disableSelection; + + var width = $("#minimap").width(); + + scale_x = width / (maxx * tile_size); + scale_y = width / (maxy * tile_size); // height is assumed to be the same + + $("#minimap").on("click", function(e) + { + var x = ((((e.clientX - 8) / scale_x) / tile_size) + 1).toFixed(0); + var y = ((maxy - (((e.clientY - 8) / scale_y) / tile_size)) + 1).toFixed(0); + + window.location.href = "byond://?src=" + hSrc + "&action=select_position&x=" + x + "&y=" + y; + }); +}); + +var updateMap = true; + +function switchTo(i) +{ + if (i == 1) + { + $("#minimap").hide(); + $("#textbased").show(); + } + else + { + $("#textbased").hide(); + $("#minimap").show(); + } +} + +function clearAll(ai) +{ + if (isAI === null) { isAI = (ai == "true"); } + $("#textbased-tbody").empty(); + $("#minimap .dot").remove(); +} + +function isHead(ijob) +{ + return (ijob % 10 == 0); // head roles always end in 0 +} + +function getColor(ijob) +{ + if (ijob == 0) { return "#C06616"; } // captain + else if (ijob >= 10 && ijob < 20) { return "#E74C3C"; } // security + else if (ijob >= 20 && ijob < 30) { return "#3498DB"; } // medical + else if (ijob >= 30 && ijob < 40) { return "#9B59B6"; } // science + else if (ijob >= 40 && ijob < 50) { return "#F1C40F"; } // engineering + else if (ijob >= 50 && ijob < 60) { return "#F39C12"; } // cargo + else if (ijob >= 200 && ijob < 230) { return "#00C100"; } // Centcom + else { return "#C38312"; } // other / unknown +} + +function add(name, assignment, ijob, life_status, dam1, dam2, dam3, dam4, area, pos_x, pos_y, in_range) +{ + try { ijob = parseInt(ijob); } + catch (ex) { ijob = 0; } + + var ls = ""; + + if (life_status === null) { ls = (life_status ? "Deceased" : "Living"); } + + var healthHTML = ""; + + if (dam1 != "" || dam2 != "" || dam3 != "" || dam4 != "") + { + var avg_dam = parseInt(dam1) + parseInt(dam2) + parseInt(dam3) + parseInt(dam4); + var i; + + if (isAI) { i = -1; } + else + { + if (avg_dam <= 0) { i = 5; } + else if (avg_dam <= 25) { i = 4; } + else if (avg_dam <= 50) { i = 3; } + else if (avg_dam <= 75) { i = 2; } + else { i = 0; } + } + + healthHTML = "
(" + dam1 + "/" + dam2 + "/" + dam3 + "/" + dam4 + ")
"; + } + else + { + healthHTML = "
Not Available
"; + } + + var trElem = $("").attr("data-ijob", ijob); + var tdElem; + var spanElem; + + tdElem = $(""); + + var italics = false; + + if (name.length >= 7 && name.substring(0, 3) == "") + { + name = name.substring(3, name.length - 4); + italics = true; + } + + spanElem = $("").text(name); + + if (italics) + { + spanElem.css("font-style", "italic"); + } + + if (isHead(ijob)) { spanElem.css("font-weight", "bold"); } + + var color = getColor(ijob); + + if (color) { spanElem.css("color", color); } + + tdElem.append(spanElem); + + if (assignment) { tdElem.append($("").text(" (" + assignment + ")")); } + + trElem.append(tdElem); + + tdElem = $(""); + tdElem.html(healthHTML); + + trElem.append(tdElem); + + tdElem = $(""); + + if (area && pos_x && pos_y) { tdElem.append($("
").text(area).addClass("tt").append($("
").append($("").text("(" + pos_x + ", " + pos_y + ")")))); } + else { tdElem.text("Not Available"); } + + trElem.append(tdElem); + + $("#textbased-tbody").append(trElem); + + if (updateMap && pos_x && pos_y && (in_range == "1")) + { + var x = parseInt(pos_x); + var y = maxy - parseInt(pos_y); + + var tx = (translate(x - 1, scale_x) - 1.5).toFixed(0); + var ty = (translate(y - 1, scale_y) + 3).toFixed(0); + + var dotElem = $("
"); + + $("#minimap").append(dotElem); + + function enable() + { + dotElem.css({ "border-color": color, "z-index": 9999, "width": "8px", "height": "8px", "margin-top": "-2px", "margin-left": "-3px" }); + } + + function disable() + { + dotElem.css({ "border-color": "transparent", "z-index": ijob, "width": "3px", "height": "3px", "margin-top": "0px", "margin-left": "0px", "filter": "" }); + } + + function click(e) + { + e.preventDefault(); + e.stopPropagation(); + + window.location.href = "byond://?src=" + hSrc + "&action=select_person&name=" + encodeURIComponent(name); + } + + trElem.on("mouseover", enable).on("mouseout", disable).on("click", click); + + dotElem.on("mouseover", function() + { + trElem.addClass("hover"); + enable(); + }).on("mouseout", function() + { + trElem.removeClass("hover"); + disable(); + }).on("click", click); + } +} + +function translate(n, scale) +{ + return (n * tile_size) * scale; +} \ No newline at end of file diff --git a/code/game/machinery/machinery.dm b/code/game/machinery/machinery.dm index fbfc193fb61..02221971859 100644 --- a/code/game/machinery/machinery.dm +++ b/code/game/machinery/machinery.dm @@ -418,7 +418,7 @@ Class Procs: return 0 // Hook for html_interface module to prevent updates to clients who don't have this as their active machine. -/obj/machinery/proc/hiIsValidClient(datum/html_interface_client/hclient) +/obj/machinery/proc/hiIsValidClient(datum/html_interface_client/hclient, datum/html_interface/hi) if (hclient.client.mob && hclient.client.mob.stat == 0) if (isAI(hclient.client.mob)) return TRUE else return hclient.client.mob.machine == src && src.Adjacent(hclient.client.mob) @@ -427,4 +427,4 @@ Class Procs: // Hook for html_interface module to unset the active machine when the window is closed by the player. /obj/machinery/proc/hiOnHide(datum/html_interface_client/hclient) - if (hclient.client.mob && hclient.client.mob.machine == src) hclient.client.mob.unset_machine() + if (hclient.client.mob && hclient.client.mob.machine == src) hclient.client.mob.unset_machine() diff --git a/code/game/objects/items/devices/sensor_device.dm b/code/game/objects/items/devices/sensor_device.dm index 2bcddf5de79..9d4bbc60b43 100644 --- a/code/game/objects/items/devices/sensor_device.dm +++ b/code/game/objects/items/devices/sensor_device.dm @@ -8,4 +8,4 @@ origin_tech = "programming=3;materials=3;magnets=3" /obj/item/device/sensor_device/attack_self(mob/user as mob) - crewmonitor(user,src) //Proc already exists, just had to call it + crewmonitor.show(user) //Proc already exists, just had to call it diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 0ce5eef9209..23e0bc6142e 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -125,7 +125,8 @@ var/list/admin_verbs_debug = list( /client/proc/test_snap_UI, /client/proc/debugNatureMapGenerator, /client/proc/check_bomb_impacts, - /proc/machine_upgrade + /proc/machine_upgrade, + /client/proc/populate_world ) var/list/admin_verbs_possess = list( /proc/possess, @@ -260,7 +261,7 @@ var/list/admin_verbs_hideable = list( /client/proc/fps, /client/proc/cmd_admin_grantfullaccess, /client/proc/cmd_admin_areatest, - /client/proc/readmin + /client/proc/readmin, ) if(holder) verbs.Remove(holder.rank.adds) @@ -595,3 +596,38 @@ var/list/admin_verbs_hideable = list( verbs -= /client/proc/readmin deadmins -= ckey return + +/client/proc/populate_world(var/amount = 50 as num) + set name = "Populate World" + set category = "Debug" + set desc = "(\"Amount of mobs to create\") Populate the world with test mobs." + + if (amount > 0) + var/area/area + var/list/candidates + var/turf/simulated/floor/tile + var/j,k + var/mob/living/carbon/human/mob + + for (var/i = 1 to amount) + j = 100 + + do + area = pick(the_station_areas) + + if (area) + + candidates = get_area_turfs(area) + + if (candidates.len) + k = 100 + + do + tile = pick(candidates) + while ((!tile || !istype(tile)) && --k > 0) + + if (tile) + mob = new/mob/living/carbon/human/interactive(tile) + + testing("Spawned test mob with name \"[mob.name]\" at [tile.x],[tile.y],[tile.z]") + while (!area && --j > 0) \ No newline at end of file diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index f1024432765..366f113a114 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -280,6 +280,11 @@ var/next_external_rsc = 0 hi = new type(null) hi.sendResources(src) + // Preload the crew monitor. This needs to be done due to BYOND bug http://www.byond.com/forum/?post=1487244 + spawn + if (crewmonitor && crewmonitor.initialized) + crewmonitor.sendResources(src) + //Send nanoui files to client SSnano.send_resources(src) getFiles( diff --git a/code/modules/games/cards.dm b/code/modules/games/cards.dm index a39c6f54dc0..379adeecace 100644 --- a/code/modules/games/cards.dm +++ b/code/modules/games/cards.dm @@ -248,5 +248,5 @@ src.toggle_conceal() // Hook for html_interface module to prevent updates to clients who don't have this in their inventory. -/obj/item/weapon/hand/proc/hiIsValidClient(datum/html_interface_client/hclient) +/obj/item/weapon/hand/proc/hiIsValidClient(datum/html_interface_client/hclient, datum/html_interface/hi) return (hclient.client.mob && hclient.client.mob.stat == 0 && (src in hclient.client.mob.contents)) diff --git a/code/modules/html_interface/html_interface.dm b/code/modules/html_interface/html_interface.dm index 0c9984f0b06..2e3359e80c4 100644 --- a/code/modules/html_interface/html_interface.dm +++ b/code/modules/html_interface/html_interface.dm @@ -54,6 +54,10 @@ Hides the HTML interface from the provided client. This will close the browser w Returns TRUE if the interface is being used (has an active client) or FALSE if not. + hi.closeAll() + +Closes the interface on all clients. + ** Additional notes ** When working with byond:// links make sure to reference the HTML interface object and NOT the original object. Topic() will still be called on @@ -74,6 +78,8 @@ mob/verb/test() */ +/var/list/html_interfaces = new/list() + /datum/html_interface // The atom we should report to. var/atom/ref @@ -100,6 +106,8 @@ mob/verb/test() var/height /datum/html_interface/New(atom/ref, title, width = 700, height = 480, head = "") + html_interfaces.Add(src) + . = ..() src.ref = ref @@ -109,9 +117,9 @@ mob/verb/test() src.head = head /datum/html_interface/Destroy() - if (src.clients) - for (var/client in src.clients) - src.hide(src.clients[client]) + src.closeAll() + + html_interfaces.Remove(src) return ..() @@ -160,7 +168,19 @@ mob/verb/test() if (istype(hclient)) if (hclient.is_loaded) hclient.client << output(list2params(list(jscript)), "browser_\ref[src].browser:eval") else - for (var/client in src.clients) src.executeJavaScript(jscript, src.clients[client]) + for (var/client in src.clients) if(src.clients[client]) src.executeJavaScript(jscript, src.clients[client]) + +/datum/html_interface/proc/callJavaScript(func, list/arguments, datum/html_interface_client/hclient = null) + if (!arguments) arguments = new/list() + + if (hclient) + hclient = getClient(hclient) + + if (istype(hclient)) + if (hclient.is_loaded) + hclient.client << output(list2params(arguments), "browser_\ref[src].browser:[func]") + else + for (var/client in src.clients) if (src.clients[client]) src.callJavaScript(func, arguments, src.clients[client]) /datum/html_interface/proc/updateLayout(layout) src.layout = layout @@ -180,7 +200,8 @@ mob/verb/test() for (var/client in src.clients) hclient = src._getClient(src.clients[client]) - if (hclient && hclient.active) src._renderContent(id, hclient, ignore_cache) + if (hclient && hclient.active) + spawn (-1) src._renderContent(id, hclient, ignore_cache) /datum/html_interface/proc/show(datum/html_interface_client/hclient) hclient = getClient(hclient, TRUE) @@ -200,6 +221,8 @@ mob/verb/test() hclient.client << output(replacetextEx(replacetextEx(file2text('html_interface.html'), "\[hsrc\]", "\ref[src]"), "", "[head]"), "browser_\ref[src].browser") winshow(hclient.client, "browser_\ref[src]", TRUE) + while (hclient.client && hclient.active && !hclient.is_loaded) sleep(2) + /datum/html_interface/proc/hide(datum/html_interface_client/hclient) hclient = getClient(hclient) @@ -243,24 +266,34 @@ mob/verb/test() /datum/html_interface/proc/isUsed() if (src.clients && src.clients.len > 0) var/datum/html_interface_client/hclient + for (var/key in clients) - hclient = clients[key] - if (hclient.active) return TRUE + hclient = _getClient(clients[key]) + + if (hclient) + if (hclient.active) return TRUE + else + clients.Remove(key) return FALSE +/datum/html_interface/proc/closeAll() + if (src.clients) + for (var/client in src.clients) + src.hide(src.clients[client]) + /* * Danger Zone */ /datum/html_interface/proc/_getClient(datum/html_interface_client/hclient) if (hclient) if (hclient.client) - if (hascall(src.ref, "hiIsValidClient")) - var/res = call(src.ref, "hiIsValidClient")(hclient) + // res = if the client has been active in the past 10 minutes and the client is allowed to view the object (context-sensitive). + var/res = hclient.client.inactivity <= 6000 && (hascall(src.ref, "hiIsValidClient") ? call(src.ref, "hiIsValidClient")(hclient, src) : TRUE) - if (res) - if (!hclient.active) src.enableFor(hclient) - else - if (hclient.active) src.disableFor(hclient) + if (res) + if (!hclient.active) src.enableFor(hclient) + else + if (hclient.active) src.disableFor(hclient) return hclient else @@ -268,8 +301,8 @@ mob/verb/test() else return null -/datum/html_interface/proc/_renderTitle(datum/html_interface_client/hclient, ignore_cache = FALSE) - if (hclient && hclient.is_loaded) +/datum/html_interface/proc/_renderTitle(datum/html_interface_client/hclient, ignore_cache = FALSE, ignore_loaded = FALSE) + if (hclient && (ignore_loaded || hclient.is_loaded)) // Only render if we have new content. if (ignore_cache || src.title != hclient.title) @@ -279,8 +312,8 @@ mob/verb/test() hclient.client << output(list2params(list(title)), "browser_\ref[src].browser:setTitle") -/datum/html_interface/proc/_renderLayout(datum/html_interface_client/hclient) - if (hclient && hclient.is_loaded) +/datum/html_interface/proc/_renderLayout(datum/html_interface_client/hclient, ignore_loaded = FALSE) + if (hclient && (ignore_loaded || hclient.is_loaded)) var/html = src.layout // Only render if we have new content. @@ -289,10 +322,10 @@ mob/verb/test() hclient.client << output(list2params(list(html)), "browser_\ref[src].browser:updateLayout") - for (var/id in src.content_elements) src._renderContent(id, hclient) + for (var/id in src.content_elements) src._renderContent(id, hclient, ignore_loaded = ignore_loaded) -/datum/html_interface/proc/_renderContent(id, datum/html_interface_client/hclient, ignore_cache = FALSE) - if (hclient && hclient.is_loaded) +/datum/html_interface/proc/_renderContent(id, datum/html_interface_client/hclient, ignore_cache = FALSE, ignore_loaded = FALSE) + if (hclient && (ignore_loaded || hclient.is_loaded)) var/html = src.content_elements[id] // Only render if we have new content. @@ -311,10 +344,11 @@ mob/verb/test() if ("onload") hclient.layout = null hclient.content_elements.len = 0 - hclient.is_loaded = TRUE - src._renderTitle(hclient, TRUE) - src._renderLayout(hclient) + src._renderTitle(hclient, TRUE, TRUE) + src._renderLayout(hclient, TRUE) + + hclient.is_loaded = TRUE if ("onclose") src.hide(hclient) diff --git a/code/modules/html_interface/nanotrasen/nanotrasen.css b/code/modules/html_interface/nanotrasen/nanotrasen.css index 2b5a2cc418c..702a381c077 100644 --- a/code/modules/html_interface/nanotrasen/nanotrasen.css +++ b/code/modules/html_interface/nanotrasen/nanotrasen.css @@ -11,6 +11,12 @@ body line-height: 170%; /* NullQuery: 170% of what? */ } +/* Fix for IE8 */ +body +{ + background-color: #1F1F1F\9 !important; +} + #ntbgcenter { position: absolute; diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 3ab80a622bb..000893f5165 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -849,7 +849,7 @@ Sorry Giacom. Please don't be mad :( return 0 if(T.z >= ZLEVEL_SPACEMAX) return 0 - if(src == user) + if(user != null && src == user) return 0 if(invisibility || alpha == 0)//cloaked return 0 diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index a0106cfcc02..f80afc9d4f8 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -389,4 +389,4 @@ return /mob/proc/update_gravity() - return + return \ No newline at end of file diff --git a/html/changelogs/NullQuery-Nullstation-hi-extended.yml b/html/changelogs/NullQuery-Nullstation-hi-extended.yml new file mode 100644 index 00000000000..de45b1a1e55 --- /dev/null +++ b/html/changelogs/NullQuery-Nullstation-hi-extended.yml @@ -0,0 +1,11 @@ +author: NullQuery +delete-after: True + +changes: + - tweak: "The crew monitoring computer, AI crew monitoring popup and the handheld crew monitor now update automatically." + - tweak: "Crew monitoring: The health information has been turned into a health indicator. It goes from green to red. Hovering over the icon shows detailed information (if available)." + - tweak: "Crew monitoring: The location coordinates have been hidden. You can see them by hovering over the 'Location' column." + - rscadd: "Crew monitoring: A minimap has been added so you can easily see where people are." + - tweak: "Crew monitoring: Players who are not in range of a camera are not shown on the minimap." + - tweak: "Crew monitoring: AI players can quickly move to other locations or track players by clicking the minimap or items in the table." + - tweak: "Crew monitoring: For AI players, health information always shows a white indicator instead of green-to-red. This is a deliberate limitation to prevent the AI from becoming too omnipresent." diff --git a/html/mapbase1024.png b/html/mapbase1024.png new file mode 100644 index 00000000000..a927a71b6e7 Binary files /dev/null and b/html/mapbase1024.png differ diff --git a/interface/interface.dm b/interface/interface.dm index ffaeadc5776..a24e86cd3aa 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -171,4 +171,11 @@ Any-Mode: (hotkey doesn't need to be on) "} src << hotkey_mode - src << other \ No newline at end of file + src << other + +// Needed to circumvent a bug where .winset does not work when used on the window.on-size event in skins. +// Used by /datum/html_interface/nanotrasen (code/modules/html_interface/nanotrasen/nanotrasen.dm) +/client/verb/_swinset(var/x as text) + set name = ".swinset" + set hidden = 1 + winset(src, null, x) diff --git a/tgstation.dme b/tgstation.dme index f12e17995b7..3789e9e11fd 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -136,6 +136,7 @@ #include "code\controllers\subsystem\npcpool.dm" #include "code\controllers\subsystem\objects.dm" #include "code\controllers\subsystem\pai.dm" +#include "code\controllers\subsystem\procqueue.dm" #include "code\controllers\subsystem\radio.dm" #include "code\controllers\subsystem\server_maintenance.dm" #include "code\controllers\subsystem\shuttles.dm"