diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 678486f0195..1b5698ba5ef 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,8 +4,9 @@ # In the event that multiple org members are to be informed of changes # to the same file or dir, add them to the end under Multiple Owners -# ShadowLarkens -/code/__DEFINES/tgui.dm @ShadowLarkens -/code/controllers/subsystem/tgui.dm @ShadowLarkens -/code/modules/tgui @ShadowLarkens -/tgui @ShadowLarkens \ No newline at end of file +# ItsSelis + +/code/__DEFINES/tgui.dm @ItsSelis +/code/controllers/subsystem/tgui.dm @ItsSelis +/code/modules/tgui @ItsSelis +/tgui @ItsSelis diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm index 14feaf1b5f8..f1a7f222484 100644 --- a/code/_onclick/hud/_defines.dm +++ b/code/_onclick/hud/_defines.dm @@ -47,6 +47,7 @@ #define ui_construct_purge "EAST:00,CENTER-1:15" #define ui_construct_fire "EAST-1:16,CENTER+1:13" //above health, slightly to the left #define ui_construct_pull "EAST-1:28,SOUTH+1:10" //above the zone_sel icon +#define ui_pai_comms "EAST-1:28,SOUTH+1:5" //Lower right, persistant menu #define ui_dropbutton "EAST-4:22,SOUTH:5" diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index a06df0c0822..03fd9bac741 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -422,16 +422,28 @@ usr.a_intent_change("right") if(I_HELP) usr.a_intent = I_HELP - usr.hud_used.action_intent.icon_state = "intent_help" + if(ispAI(usr)) + usr.a_intent_change(I_HELP) + else + usr.hud_used.action_intent.icon_state = "intent_help" if(I_HURT) usr.a_intent = I_HURT - usr.hud_used.action_intent.icon_state = "intent_harm" + if(ispAI(usr)) + usr.a_intent_change(I_HURT) + else + usr.hud_used.action_intent.icon_state = "intent_harm" if(I_GRAB) usr.a_intent = I_GRAB - usr.hud_used.action_intent.icon_state = "intent_grab" + if(ispAI(usr)) + usr.a_intent_change(I_GRAB) + else + usr.hud_used.action_intent.icon_state = "intent_grab" if(I_DISARM) usr.a_intent = I_DISARM - usr.hud_used.action_intent.icon_state = "intent_disarm" + if(ispAI(usr)) + usr.a_intent_change(I_DISARM) + else + usr.hud_used.action_intent.icon_state = "intent_disarm" if("pull") usr.stop_pulling() diff --git a/code/_onclick/hud/screen_objects_vr.dm b/code/_onclick/hud/screen_objects_vr.dm index 4d837803279..6ea02dd34a3 100644 --- a/code/_onclick/hud/screen_objects_vr.dm +++ b/code/_onclick/hud/screen_objects_vr.dm @@ -65,6 +65,43 @@ if(istype(H) && istype(H.species, /datum/species/xenochimera)) // If you're somehow able to click this while not a chimera, this should prevent weird runtimes. Will need changing if regeneration is ever opened to non-chimera using the same alert. if(H.revive_ready == REVIVING_DONE) // Sanity check. H.hatch() // Hatch. + //pAI buttons!!! + if("fold/unfold") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + if(p.loc == p.card) + p.fold_out() + else + p.fold_up() + if("choose chassis") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + p.choose_chassis() + + if("software interface") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + p.paiInterface() + + if("radio configuration") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + p.radio.tgui_interact(p) + + if("pda") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + p.pda.cmd_pda_open_ui() + + if("communicator") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + p.communicator.activate() + + if("known languages") + if(ispAI(usr)) + var/mob/living/silicon/pai/p = usr + p.check_languages() else return 0 diff --git a/code/controllers/subsystems/vote.dm b/code/controllers/subsystems/vote.dm index 658cff17164..61ba792151f 100644 --- a/code/controllers/subsystems/vote.dm +++ b/code/controllers/subsystems/vote.dm @@ -362,7 +362,12 @@ SUBSYSTEM_DEF(vote) if(VOTE_RESTART) if(config.allow_vote_restart || usr.client.holder) - initiate_vote(VOTE_RESTART, usr.key) + var/admin_number_present = send2irc_adminless_only(usr.ckey, usr) + if(admin_number_present <= 0 || usr.client.holder) + if(tgui_alert(usr, "Are you sure you want to start a RESTART VOTE? You should only do this if the server is dying and no staff are around to investigate.", "RESTART VOTE", list("No", "Yes I want to start a RESTART VOTE")) == "Yes I want to start a RESTART VOTE") + initiate_vote(VOTE_RESTART, usr.key) + else + to_chat(usr, "You can't start a RESTART VOTE while there are staff around. If you are having an issue with the round, please ahelp it.") if(VOTE_GAMEMODE) if(config.allow_vote_mode || usr.client.holder) initiate_vote(VOTE_GAMEMODE, usr.key) diff --git a/code/datums/supplypacks/misc_vr.dm b/code/datums/supplypacks/misc_vr.dm index 63f055d2d45..539e88f75b2 100644 --- a/code/datums/supplypacks/misc_vr.dm +++ b/code/datums/supplypacks/misc_vr.dm @@ -102,6 +102,16 @@ containername = "Commonwealth engineering hardsuit crate" access = access_engine +/datum/supply_pack/misc/breacher_rig + name = "unathi breacher hardsuit (empty)" + contains = list( + /obj/item/weapon/rig/breacher = 1 + ) + cost = 250 + containertype = /obj/structure/closet/crate/secure/gear + containername = "unathi breacher hardsuit crate" + access = access_armory + /datum/supply_pack/misc/zero_rig name = "null hardsuit (jets)" contains = list( diff --git a/code/game/objects/items/devices/communicator/messaging.dm b/code/game/objects/items/devices/communicator/messaging.dm index 4195d1db98b..a9bc3afe6db 100644 --- a/code/game/objects/items/devices/communicator/messaging.dm +++ b/code/game/objects/items/devices/communicator/messaging.dm @@ -95,7 +95,20 @@ L = loc if(L) - to_chat(L, "[bicon(src)] Message from [who].") + to_chat(L, "[bicon(src)] Message from [who]: \"[text]\" (Reply)") + +// This is the only Topic the communicators really uses +/obj/item/device/communicator/Topic(href, href_list) + switch(href_list["action"]) + if("Reply") + var/obj/item/device/communicator/comm = locate(href_list["target"]) + var/message = input(usr, "Enter your message below.", "Reply") + + if(message) + exonet.send_message(comm.exonet.address, "text", message) + im_list += list(list("address" = exonet.address, "to_address" = comm.exonet.address, "im" = message)) + log_pda("(COMM: [src]) sent \"[message]\" to [exonet.get_atom_from_address(comm.exonet.address)]", usr) + to_chat(usr, "[bicon(src)] Sent message to [comm.owner], \"[message]\"") // Verb: text_communicator() // Parameters: None diff --git a/code/game/objects/items/devices/paicard.dm b/code/game/objects/items/devices/paicard.dm index 23d7452f3d8..0a73ea74c64 100644 --- a/code/game/objects/items/devices/paicard.dm +++ b/code/game/objects/items/devices/paicard.dm @@ -16,6 +16,7 @@ GLOBAL_LIST_BOILERPLATE(all_pai_cards, /obj/item/device/paicard) var/mob/living/silicon/pai/pai var/image/screen_layer var/screen_color = "#00ff0d" + var/last_notify = 0 /obj/item/device/paicard/relaymove(var/mob/user, var/direction) if(user.stat || user.stunned) @@ -44,26 +45,52 @@ GLOBAL_LIST_BOILERPLATE(all_pai_cards, /obj/item/device/paicard) if(choice == "No") return ..() - var/pai_name = input(user, "Choose your character's name", "Character Name") as text - var/actual_pai_name = sanitize_name(pai_name, ,1) - if(isnull(actual_pai_name)) - return ..() - + choice = tgui_alert(user, "Do you want to load your pAI data?", "Load", list("Yes", "No")) + var/actual_pai_name var/turf/location = get_turf(src) - if(istype(src , /obj/item/device/paicard/typeb)) - var/obj/item/device/paicard/typeb/card = new(location) - var/mob/living/silicon/pai/new_pai = new(card) - new_pai.key = user.key - card.setPersonality(new_pai) - new_pai.SetName(actual_pai_name) - else - var/obj/item/device/paicard/card = new(location) - var/mob/living/silicon/pai/new_pai = new(card) - new_pai.key = user.key - card.setPersonality(new_pai) - new_pai.SetName(actual_pai_name) + if(choice == "No") + var/pai_name = input(user, "Choose your character's name", "Character Name") as text + actual_pai_name = sanitize_name(pai_name, ,1) + if(isnull(actual_pai_name)) + return ..() + if(istype(src , /obj/item/device/paicard/typeb)) + var/obj/item/device/paicard/typeb/card = new(location) + var/mob/living/silicon/pai/new_pai = new(card) + new_pai.key = user.key + card.setPersonality(new_pai) + new_pai.SetName(actual_pai_name) + else + var/obj/item/device/paicard/card = new(location) + var/mob/living/silicon/pai/new_pai = new(card) + new_pai.key = user.key + card.setPersonality(new_pai) + new_pai.SetName(actual_pai_name) + + if(choice == "Yes") + if(istype(src , /obj/item/device/paicard/typeb)) + var/obj/item/device/paicard/typeb/card = new(location) + var/mob/living/silicon/pai/new_pai = new(card) + new_pai.key = user.key + card.setPersonality(new_pai) + if(!new_pai.savefile_load(new_pai)) + var/pai_name = input(new_pai, "Choose your character's name", "Character Name") as text + actual_pai_name = sanitize_name(pai_name, ,1) + if(isnull(actual_pai_name)) + return ..() + else + var/obj/item/device/paicard/card = new(location) + var/mob/living/silicon/pai/new_pai = new(card) + new_pai.key = user.key + card.setPersonality(new_pai) + if(!new_pai.savefile_load(new_pai)) + var/pai_name = input(new_pai, "Choose your character's name", "Character Name") as text + actual_pai_name = sanitize_name(pai_name, ,1) + if(isnull(actual_pai_name)) + return ..() + qdel(src) return ..() + // VOREStation Edit End /obj/item/device/paicard/attack_self(mob/user) @@ -352,9 +379,11 @@ GLOBAL_LIST_BOILERPLATE(all_pai_cards, /obj/item/device/paicard) current_emotion = emotion /obj/item/device/paicard/proc/alertUpdate() - var/turf/T = get_turf_or_move(src.loc) - for (var/mob/M in viewers(T)) - M.show_message("\The [src] flashes a message across its screen, \"Additional personalities available for download.\"", 3, "\The [src] bleeps electronically.", 2) + if(pai) + return + if(last_notify == 0 || (5 MINUTES <= world.time - last_notify)) + audible_message("\The [src] flashes a message across its screen, \"Additional personalities available for download.\"", hearing_distance = world.view, runemessage = "bleeps!") + last_notify = world.time /obj/item/device/paicard/emp_act(severity) for(var/mob/M in src) diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm index 6a5e2fd954e..288652c31f5 100644 --- a/code/game/objects/structures/crates_lockers/crates.dm +++ b/code/game/objects/structures/crates_lockers/crates.dm @@ -90,7 +90,9 @@ src.set_dir(turn(src.dir, 90)) /obj/structure/closet/crate/attackby(obj/item/weapon/W as obj, mob/user as mob) - if(opened) + if(W.is_wrench() && istype(src,/obj/structure/closet/crate/bin)) + return ..() + else if(opened) if(isrobot(user)) return if(W.loc != user) // This should stop mounted modules ending up outside the module. diff --git a/code/modules/admin/admin_verb_lists_vr.dm b/code/modules/admin/admin_verb_lists_vr.dm index 9f4d81120c4..b871c3315bd 100644 --- a/code/modules/admin/admin_verb_lists_vr.dm +++ b/code/modules/admin/admin_verb_lists_vr.dm @@ -112,7 +112,7 @@ var/list/admin_verbs_admin = list( /client/proc/change_human_appearance_self, // Allows the human-based mob itself change its basic appearance , /client/proc/change_security_level, /client/proc/view_chemical_reaction_logs, - /client/proc/makePAI, + /client/proc/makepAI, /client/proc/toggle_debug_logs, /client/proc/toggle_attack_logs, /datum/admins/proc/paralyze_mob, @@ -519,7 +519,7 @@ var/list/admin_verbs_event_manager = list( /client/proc/change_human_appearance_admin, // Allows an admin to change the basic appearance of human-based mobs , /client/proc/change_human_appearance_self, // Allows the human-based mob itself change its basic appearance , /client/proc/change_security_level, - /client/proc/makePAI, + /client/proc/makepAI, /client/proc/toggle_debug_logs, /client/proc/toggle_attack_logs, /datum/admins/proc/paralyze_mob, diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index e9d63621b06..db51bd4ed33 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -35,11 +35,24 @@ set category = "Admin" set name = "Aghost" if(!holder) return + + var/build_mode + if(src.buildmode) + build_mode = tgui_alert(src, "You appear to be currently in buildmode. Do you want to re-enter buildmode after aghosting?", "Buildmode", list("Yes", "No")) + if(build_mode != "Yes") + to_chat(src, "Will not re-enter buildmode after switch.") + if(istype(mob,/mob/observer/dead)) //re-enter var/mob/observer/dead/ghost = mob if(ghost.can_reenter_corpse) - ghost.reenter_corpse() + if(build_mode) + togglebuildmode(mob) + ghost.reenter_corpse() + if(build_mode == "Yes") + togglebuildmode(mob) + else + ghost.reenter_corpse() else to_chat(ghost, "Error: Aghost: Can't reenter corpse.") return @@ -51,8 +64,16 @@ else //ghostize var/mob/body = mob - var/mob/observer/dead/ghost = body.ghostize(1) - ghost.admin_ghosted = 1 + var/mob/observer/dead/ghost + if(build_mode) + togglebuildmode(body) + ghost = body.ghostize(1) + ghost.admin_ghosted = 1 + if(build_mode == "Yes") + togglebuildmode(ghost) + else + ghost = body.ghostize(1) + ghost.admin_ghosted = 1 if(body) body.teleop = ghost if(!body.key) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index c82ea1c80ad..3a8b32585af 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -151,7 +151,7 @@ switch(new_rank) if(null,"") return if("*New Rank*") - new_rank = input(usr, "Please input a new rank", "New custom rank", null, null) as null|text + new_rank = input(usr, "Please input a new rank", "New custom rank") as null|text if(config.admin_legacy_system) new_rank = ckeyEx(new_rank) if(!new_rank) diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 8b0baa59676..40635bb7feb 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -129,31 +129,35 @@ M.Animalize() -/client/proc/makepAI(var/turf/T in mob_list) +/client/proc/makepAI() set category = "Fun" set name = "Make pAI" - set desc = "Specify a location to spawn a pAI device, then specify a key to play that pAI" + set desc = "Spawn someone in as a pAI!" + if(!check_rights(R_ADMIN|R_EVENT|R_DEBUG)) + return + var/turf/T = get_turf(mob) var/list/available = list() for(var/mob/C in mob_list) - if(C.key) + if(C.key && isobserver(C)) available.Add(C) var/mob/choice = tgui_input_list(usr, "Choose a player to play the pAI", "Spawn pAI", available) if(!choice) return 0 - if(!istype(choice, /mob/observer/dead)) - var/confirm = tgui_alert(usr, "[choice.key] isn't ghosting right now. Are you sure you want to yank them out of them out of their body and place them in this pAI?", "Spawn pAI Confirmation", list("No", "Yes")) - if(confirm != "Yes") - return 0 - var/obj/item/device/paicard/card = new(T) + var/obj/item/device/paicard/typeb/card = new(T) var/mob/living/silicon/pai/pai = new(card) - pai.name = sanitizeSafe(input(choice, "Enter your pAI name:", "pAI Name", "Personal AI") as text) pai.real_name = pai.name pai.key = choice.key card.setPersonality(pai) + if(tgui_alert(pai, "Do you want to load your pAI data?", "Load", list("Yes", "No")) == "Yes") + pai.savefile_load(pai) + else + pai.name = sanitizeSafe(input(pai, "Enter your pAI name:", "pAI Name", "Personal AI") as text) + card.setPersonality(pai) for(var/datum/paiCandidate/candidate in paiController.pai_candidates) if(candidate.key == choice.key) paiController.pai_candidates.Remove(candidate) + log_admin("made a pAI with key=[pai.key] at ([T.x],[T.y],[T.z])") feedback_add_details("admin_verb","MPAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/cmd_admin_alienize(var/mob/M in mob_list) diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 68d5bbf2657..0d3837b2094 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -530,6 +530,15 @@ "stellardelight_nanomap_z1.png" = 'icons/_nanomaps/sd_deck1.png', "stellardelight_nanomap_z2.png" = 'icons/_nanomaps/sd_deck2.png', "stellardelight_nanomap_z3.png" = 'icons/_nanomaps/sd_deck3.png', + "groundbase_nanomap_z1.png" = 'icons/_nanomaps/gb1.png', + "groundbase_nanomap_z2.png" = 'icons/_nanomaps/gb2.png', + "groundbase_nanomap_z3.png" = 'icons/_nanomaps/gb3.png', + "groundbase_nanomap_z4.png" = 'icons/_nanomaps/gbnorth.png', + "groundbase_nanomap_z5.png" = 'icons/_nanomaps/gbsouth.png', + "groundbase_nanomap_z6.png" = 'icons/_nanomaps/gbeast.png', + "groundbase_nanomap_z7.png" = 'icons/_nanomaps/gbwest.png', + "groundbase_nanomap_z10.png" = 'icons/_nanomaps/gbmining.png', + // VOREStation Edit End ) \ No newline at end of file diff --git a/code/modules/clothing/spacesuits/rig/suits/alien.dm b/code/modules/clothing/spacesuits/rig/suits/alien.dm index 89d854b370b..3e301b6078f 100644 --- a/code/modules/clothing/spacesuits/rig/suits/alien.dm +++ b/code/modules/clothing/spacesuits/rig/suits/alien.dm @@ -7,7 +7,7 @@ desc = "A cheap NT knock-off of an Unathi battle-rig. Looks like a fish, moves like a fish, steers like a cow." suit_type = "\improper NT breacher" icon_state = "breacher_rig_cheap" - armor = list(melee = 60, bullet = 60, laser = 60, energy = 60, bomb = 70, bio = 100, rad = 50) + armor = list(melee = 60, bullet = 45, laser = 35, energy = 35, bomb = 70, bio = 100, rad = 50) emp_protection = -20 slowdown = 6 offline_slowdown = 10 diff --git a/code/modules/emotes/definitions/audible.dm b/code/modules/emotes/definitions/audible.dm index 4df078aa11b..d702a32d3a8 100644 --- a/code/modules/emotes/definitions/audible.dm +++ b/code/modules/emotes/definitions/audible.dm @@ -205,6 +205,14 @@ emote_message_3p = "purrs." emote_sound = 'sound/voice/cat_purr_long.ogg' +/decl/emote/audible/fennecscream + key = "fennecscream" + emote_message_3p = "screeches!" + +/decl/emote/audible/zoom + key = "zoom" + emote_message_3p = "zooms." + /decl/emote/audible/teshsqueak key = "surprised" emote_message_1p = "You chirp in surprise!" diff --git a/code/modules/emotes/definitions/synthetics.dm b/code/modules/emotes/definitions/synthetics.dm index 32e7bdf5045..4f94b7b59b6 100644 --- a/code/modules/emotes/definitions/synthetics.dm +++ b/code/modules/emotes/definitions/synthetics.dm @@ -63,6 +63,7 @@ emote_message_3p_target = "boops at TARGET!" emote_message_3p = "boops." emote_sound = 'sound/voice/roboboop.ogg' + sound_vary = TRUE /decl/emote/audible/synth/robochirp key = "robochirp" @@ -71,3 +72,4 @@ emote_message_3p_target = "chirps at TARGET!" emote_message_3p = "chirps." emote_sound = 'sound/voice/robochirp.ogg' + sound_vary = TRUE diff --git a/code/modules/emotes/emote_define.dm b/code/modules/emotes/emote_define.dm index 14e7481d5d7..7aed1e982f3 100644 --- a/code/modules/emotes/emote_define.dm +++ b/code/modules/emotes/emote_define.dm @@ -44,7 +44,7 @@ var/global/list/emotes_by_key var/emote_range = 0 // If >0, restricts emote visibility to viewers within range. var/sound_preferences = list(/datum/client_preference/emote_noises) // Default emote sound_preferences is just emote_noises. Belch emote overrides this list for pref-checks. - var/sound_vary = TRUE + var/sound_vary = FALSE /decl/emote/Initialize() . = ..() diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 2d991e72904..f6421312784 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -984,8 +984,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp var/obj/item/device/paicard/PP = p if(PP.pai == null) count++ - PP.icon = 'icons/obj/pda_vr.dmi' // VOREStation Edit PP.add_overlay("pai-ghostalert") + PP.alertUpdate() spawn(54) PP.cut_overlays() to_chat(usr,"Flashing the displays of [count] unoccupied PAIs.") diff --git a/code/modules/mob/holder.dm b/code/modules/mob/holder.dm index 5b6d1556402..b2cd769b5ba 100644 --- a/code/modules/mob/holder.dm +++ b/code/modules/mob/holder.dm @@ -29,11 +29,13 @@ var/list/holder_mob_icon_cache = list() ASSERT(ismob(held)) . = ..() held.forceMove(src) + held.reset_view(src) START_PROCESSING(SSobj, src) /obj/item/weapon/holder/Entered(mob/held, atom/OldLoc) if(held_mob) held.forceMove(get_turf(src)) + held.reset_view(null) return ASSERT(ismob(held)) . = ..() @@ -71,6 +73,7 @@ var/list/holder_mob_icon_cache = list() held_mob.transform = original_transform held_mob.vis_flags = original_vis_flags held_mob.forceMove(get_turf(src)) + held_mob.reset_view(null) held_mob = null /obj/item/weapon/holder/throw_at(atom/target, range, speed, thrower) @@ -103,9 +106,12 @@ var/list/holder_mob_icon_cache = list() holster.clear_holster() to_chat(held, "You extricate yourself from [holster].") forceMove(get_turf(src)) + held.reset_view(null) + else if(isitem(loc)) to_chat(held, "You struggle free of [loc].") forceMove(get_turf(src)) + held.reset_view(null) //Mob specific holders. /obj/item/weapon/holder/diona @@ -167,6 +173,12 @@ var/list/holder_mob_icon_cache = list() item_state = "cat" /obj/item/weapon/holder/cat/runtime + +/obj/item/holder/fennec + origin_tech = list(TECH_BIO = 2) + +/obj/item/holder/cat/runtime + origin_tech = list(TECH_BIO = 2, TECH_DATA = 4) /obj/item/weapon/holder/cat/cak diff --git a/code/modules/mob/living/carbon/human/emote.dm b/code/modules/mob/living/carbon/human/emote.dm index 47cc1e8f574..028fc518e23 100644 --- a/code/modules/mob/living/carbon/human/emote.dm +++ b/code/modules/mob/living/carbon/human/emote.dm @@ -143,7 +143,9 @@ var/list/_human_default_emotes = list( /decl/emote/audible/coyawoo2, /decl/emote/audible/coyawoo3, /decl/emote/audible/coyawoo4, - /decl/emote/audible/coyawoo5 + /decl/emote/audible/coyawoo5, + /decl/emote/audible/fennecscream, + /decl/emote/audible/zoom //VOREStation Add End ) @@ -273,7 +275,10 @@ var/list/_simple_mob_default_emotes = list( /decl/emote/visible/blep, /decl/emote/audible/prbt, /decl/emote/audible/gyoh, - /decl/emote/audible/rumble + /decl/emote/audible/rumble, + /decl/emote/audible/fennecscream, + /decl/emote/audible/zoom + ) //VOREStation Add End diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm index f13051be24b..9293eec4738 100644 --- a/code/modules/mob/living/carbon/human/human_movement.dm +++ b/code/modules/mob/living/carbon/human/human_movement.dm @@ -289,3 +289,8 @@ playsound(T, S, volume, FALSE) return + +/mob/living/carbon/human/set_dir(var/new_dir) + . = ..() + if(. && species.tail) + update_tail_showing() \ No newline at end of file diff --git a/code/modules/mob/living/carbon/human/species/station/alraune.dm b/code/modules/mob/living/carbon/human/species/station/alraune.dm index 12424f993c3..999425d4e6b 100644 --- a/code/modules/mob/living/carbon/human/species/station/alraune.dm +++ b/code/modules/mob/living/carbon/human/species/station/alraune.dm @@ -2,7 +2,7 @@ name = SPECIES_ALRAUNE name_plural = "Alraunes" unarmed_types = list(/datum/unarmed_attack/stomp, /datum/unarmed_attack/kick, /datum/unarmed_attack/punch, /datum/unarmed_attack/bite) - num_alternate_languages = 2 + num_alternate_languages = 3 slowdown = 1 //slow, they're plants. Not as slow as full diona. total_health = 100 //standard brute_mod = 1 //nothing special diff --git a/code/modules/mob/living/carbon/human/species/station/teshari.dm b/code/modules/mob/living/carbon/human/species/station/teshari.dm index 6f243418895..2917dfcf39e 100644 --- a/code/modules/mob/living/carbon/human/species/station/teshari.dm +++ b/code/modules/mob/living/carbon/human/species/station/teshari.dm @@ -59,7 +59,7 @@ ambiguous_genders = TRUE - spawn_flags = SPECIES_CAN_JOIN | SPECIES_IS_WHITELISTED | SPECIES_NO_POSIBRAIN + spawn_flags = SPECIES_CAN_JOIN | SPECIES_IS_WHITELISTED appearance_flags = HAS_HAIR_COLOR | HAS_SKIN_COLOR | HAS_EYE_COLOR bump_flag = MONKEY swap_flags = MONKEY|SLIME|SIMPLE_ANIMAL diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm index 889ed2fdfa9..248439381c3 100644 --- a/code/modules/mob/living/carbon/human/update_icons.dm +++ b/code/modules/mob/living/carbon/human/update_icons.dm @@ -66,37 +66,40 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() #define MOB_DAM_LAYER 4 //Injury overlay sprites like open wounds #define SURGERY_LAYER 5 //Overlays for open surgical sites #define UNDERWEAR_LAYER 6 //Underwear/bras/etc -#define SHOES_LAYER_ALT 7 //Shoe-slot item (when set to be under uniform via verb) -#define UNIFORM_LAYER 8 //Uniform-slot item -#define ID_LAYER 9 //ID-slot item -#define SHOES_LAYER 10 //Shoe-slot item -#define GLOVES_LAYER 11 //Glove-slot item -#define BELT_LAYER 12 //Belt-slot item -#define SUIT_LAYER 13 //Suit-slot item -#define TAIL_LAYER 14 //Some species have tails to render -#define GLASSES_LAYER 15 //Eye-slot item -#define BELT_LAYER_ALT 16 //Belt-slot item (when set to be above suit via verb) -#define SUIT_STORE_LAYER 17 //Suit storage-slot item -#define BACK_LAYER 18 //Back-slot item -#define HAIR_LAYER 19 //The human's hair -#define HAIR_ACCESSORY_LAYER 20 //VOREStation edit. Simply move this up a number if things are added. -#define EARS_LAYER 21 //Both ear-slot items (combined image) -#define EYES_LAYER 22 //Mob's eyes (used for glowing eyes) -#define FACEMASK_LAYER 23 //Mask-slot item -#define HEAD_LAYER 24 //Head-slot item -#define HANDCUFF_LAYER 25 //Handcuffs, if the human is handcuffed, in a secret inv slot -#define LEGCUFF_LAYER 26 //Same as handcuffs, for legcuffs -#define L_HAND_LAYER 27 //Left-hand item -#define R_HAND_LAYER 28 //Right-hand item -#define WING_LAYER 29 //Wings or protrusions over the suit. -#define TAIL_LAYER_ALT 30 //Modified tail-sprite layer. Tend to be larger. -#define MODIFIER_EFFECTS_LAYER 31 //Effects drawn by modifiers -#define FIRE_LAYER 32 //'Mob on fire' overlay layer -#define MOB_WATER_LAYER 33 //'Mob submerged' overlay layer -#define TARGETED_LAYER 34 //'Aimed at' overlay layer -#define TOTAL_LAYERS 34 //VOREStation edit. <---- KEEP THIS UPDATED, should always equal the highest number here, used to initialize a list. +#define TAIL_SOUTH_LAYER 7 //Tail as viewed from the south +#define SHOES_LAYER_ALT 8 //Shoe-slot item (when set to be under uniform via verb) +#define UNIFORM_LAYER 9 //Uniform-slot item +#define ID_LAYER 10 //ID-slot item +#define SHOES_LAYER 11 //Shoe-slot item +#define GLOVES_LAYER 12 //Glove-slot item +#define BELT_LAYER 13 //Belt-slot item +#define SUIT_LAYER 14 //Suit-slot item +#define TAIL_NORTH_LAYER 15 //Some species have tails to render (As viewed from the N, E, or W) +#define GLASSES_LAYER 16 //Eye-slot item +#define BELT_LAYER_ALT 17 //Belt-slot item (when set to be above suit via verb) +#define SUIT_STORE_LAYER 18 //Suit storage-slot item +#define BACK_LAYER 19 //Back-slot item +#define HAIR_LAYER 20 //The human's hair +#define HAIR_ACCESSORY_LAYER 21 //VOREStation edit. Simply move this up a number if things are added. +#define EARS_LAYER 22 //Both ear-slot items (combined image) +#define EYES_LAYER 23 //Mob's eyes (used for glowing eyes) +#define FACEMASK_LAYER 24 //Mask-slot item +#define HEAD_LAYER 25 //Head-slot item +#define HANDCUFF_LAYER 26 //Handcuffs, if the human is handcuffed, in a secret inv slot +#define LEGCUFF_LAYER 27 //Same as handcuffs, for legcuffs +#define L_HAND_LAYER 28 //Left-hand item +#define R_HAND_LAYER 29 //Right-hand item +#define WING_LAYER 30 //Wings or protrusions over the suit. +#define TAIL_LAYER_ALT 31 //Modified tail-sprite layer. Tend to be larger. +#define MODIFIER_EFFECTS_LAYER 32 //Effects drawn by modifiers +#define FIRE_LAYER 33 //'Mob on fire' overlay layer +#define MOB_WATER_LAYER 34 //'Mob submerged' overlay layer +#define TARGETED_LAYER 35 //'Aimed at' overlay layer +#define TOTAL_LAYERS 35 //VOREStation edit. <---- KEEP THIS UPDATED, should always equal the highest number here, used to initialize a list. ////////////////////////////////// +#define GET_TAIL_LAYER (dir == NORTH ? TAIL_NORTH_LAYER : TAIL_SOUTH_LAYER) + /mob/living/carbon/human var/list/overlays_standing[TOTAL_LAYERS] var/previous_damage_appearance // store what the body last looked like, so we only have to update it if something changed @@ -133,6 +136,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() var/desired_scale_y = size_multiplier * icon_scale_y desired_scale_x *= species.icon_scale_x desired_scale_y *= species.icon_scale_y + vis_height = species.icon_height appearance_flags |= PIXEL_SCALE if(fuzzy) appearance_flags &= ~PIXEL_SCALE @@ -271,7 +275,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() for(var/M in part.markings) icon_key += "[M][part.markings[M]["color"]]" - + // VOREStation Edit Start if(part.nail_polish) icon_key += "_[part.nail_polish.icon]_[part.nail_polish.icon_state]_[part.nail_polish.color]" @@ -369,6 +373,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() update_tail_showing() update_wing_showing() + /mob/living/carbon/human/proc/update_skin() if(QDESTROYING(src)) return @@ -488,7 +493,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() face_standing.Blend(ears_s, ICON_OVERLAY) if(ear_style?.em_block) em_block_ears = em_block_image_generic(image(ears_s)) - + var/image/semifinal = image(face_standing, layer = BODY_LAYER+HAIR_LAYER, "pixel_y" = head_organ.head_offset) if(em_block_ears) semifinal.overlays += em_block_ears // Leaving this as overlays += @@ -831,7 +836,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() suit_sprite = INV_SUIT_DEF_ICON var/icon/c_mask = null - var/tail_is_rendered = (overlays_standing[TAIL_LAYER] || overlays_standing[TAIL_LAYER_ALT]) + var/tail_is_rendered = (overlays_standing[TAIL_NORTH_LAYER] || overlays_standing[TAIL_SOUTH_LAYER]) var/valid_clip_mask = tail_style?.clip_mask if(tail_is_rendered && valid_clip_mask && !(istype(suit) && suit.taurized)) //Clip the lower half of the suit off using the tail's clip mask for taurs since taur bodies aren't hidden. @@ -948,16 +953,16 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() if(QDESTROYING(src)) return - remove_layer(TAIL_LAYER) - remove_layer(TAIL_LAYER_ALT) // Alt Tail Layer + remove_layer(TAIL_NORTH_LAYER) + remove_layer(TAIL_SOUTH_LAYER) - var/used_tail_layer = tail_alt ? TAIL_LAYER_ALT : TAIL_LAYER + var tail_layer = GET_TAIL_LAYER var/image/tail_image = get_tail_image() if(tail_image) - tail_image.layer = BODY_LAYER+used_tail_layer - overlays_standing[used_tail_layer] = tail_image - apply_layer(used_tail_layer) + tail_image.layer = BODY_LAYER+tail_layer + overlays_standing[tail_layer] = tail_image + apply_layer(tail_layer) return var/species_tail = species.get_tail(src) // Species tail icon_state prefix. @@ -965,7 +970,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() //This one is actually not that bad I guess. if(species_tail && !(wear_suit && wear_suit.flags_inv & HIDETAIL)) var/icon/tail_s = get_tail_icon() - overlays_standing[used_tail_layer] = image(icon = tail_s, icon_state = "[species_tail]_s", layer = BODY_LAYER+used_tail_layer) // Alt Tail Layer + overlays_standing[tail_layer] = image(icon = tail_s, icon_state = "[species_tail]_s", layer = BODY_LAYER+tail_layer) animate_tail_reset() //TODO: Is this the appropriate place for this, and not on species...? @@ -990,19 +995,19 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() return tail_icon /mob/living/carbon/human/proc/set_tail_state(var/t_state) - var/used_tail_layer = tail_alt ? TAIL_LAYER_ALT : TAIL_LAYER // Alt Tail Layer - var/image/tail_overlay = overlays_standing[used_tail_layer] + var/tail_layer = GET_TAIL_LAYER + var/image/tail_overlay = overlays_standing[tail_layer] - remove_layer(TAIL_LAYER) - remove_layer(TAIL_LAYER_ALT) + remove_layer(TAIL_NORTH_LAYER) + remove_layer(TAIL_SOUTH_LAYER) if(tail_overlay) - overlays_standing[used_tail_layer] = tail_overlay + overlays_standing[tail_layer] = tail_overlay if(species.get_tail_animation(src)) tail_overlay.icon_state = t_state . = tail_overlay - apply_layer(used_tail_layer) + apply_layer(tail_layer) //Not really once, since BYOND can't do that. //Update this if the ability to flick() images or make looping animation start at the first frame is ever added. @@ -1012,9 +1017,9 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() return var/t_state = "[species.get_tail(src)]_once" - var/used_tail_layer = tail_alt ? TAIL_LAYER_ALT : TAIL_LAYER // Alt Tail Layer + var/tail_layer = GET_TAIL_LAYER - var/image/tail_overlay = overlays_standing[used_tail_layer] // Alt Tail Layer + var/image/tail_overlay = overlays_standing[tail_layer] if(tail_overlay && tail_overlay.icon_state == t_state) return //let the existing animation finish @@ -1022,7 +1027,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() if(tail_overlay) spawn(20) //check that the animation hasn't changed in the meantime - if(overlays_standing[used_tail_layer] == tail_overlay && tail_overlay.icon_state == t_state) // Alt Tail Layer + if(overlays_standing[tail_layer] == tail_overlay && tail_overlay.icon_state == t_state) animate_tail_stop() /mob/living/carbon/human/proc/animate_tail_start() @@ -1176,7 +1181,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() var/icon/ears_s = new/icon("icon" = synthetic.icon, "icon_state" = "ears") ears_s.Blend(rgb(src.r_ears, src.g_ears, src.b_ears), species.color_mult ? ICON_MULTIPLY : ICON_ADD) return ears_s - + if(ear_style && !(head && (head.flags_inv & BLOCKHEADHAIR))) var/icon/ears_s = new/icon("icon" = ear_style.icon, "icon_state" = ear_style.icon_state) if(ear_style.do_colouration) @@ -1272,7 +1277,9 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() #undef GLOVES_LAYER #undef BELT_LAYER #undef SUIT_LAYER -#undef TAIL_LAYER +#undef TAIL_NORTH_LAYER +#undef TAIL_SOUTH_LAYER +#undef GET_TAIL_LAYER #undef GLASSES_LAYER #undef BELT_LAYER_ALT #undef SUIT_STORE_LAYER diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 09147c527f0..f62dbb8ec59 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1206,9 +1206,13 @@ if(!screen_icon) screen_icon = new() RegisterSignal(screen_icon, COMSIG_CLICK, .proc/character_setup_click) - screen_icon.icon = HUD.ui_style - screen_icon.color = HUD.ui_color - screen_icon.alpha = HUD.ui_alpha + if(ispAI(user)) + screen_icon.icon = 'icons/mob/pai_hud.dmi' + screen_icon.screen_loc = ui_acti + else + screen_icon.icon = HUD.ui_style + screen_icon.color = HUD.ui_color + screen_icon.alpha = HUD.ui_alpha LAZYADD(HUD.other_important, screen_icon) user.client?.screen += screen_icon diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index 4fce3c84ea9..84b825b6444 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -141,7 +141,7 @@ var/list/channel_to_radio_key = new return "asks" return verb -/mob/living/say(var/message, var/whispering = 0) +/mob/living/say(var/message, var/datum/language/speaking = null, var/whispering = 0) //If you're muted for IC chat if(client) if(message) diff --git a/code/modules/mob/living/silicon/pai/examine.dm b/code/modules/mob/living/silicon/pai/examine.dm index b6890470c98..9c009dcf6f4 100644 --- a/code/modules/mob/living/silicon/pai/examine.dm +++ b/code/modules/mob/living/silicon/pai/examine.dm @@ -9,16 +9,10 @@ // VOREStation Edit: Start . += attempt_vr(src,"examine_bellies",args) //VOREStation Edit - if(ooc_notes) - . += "OOC Notes: \[View\]" - // VOREStation Edit: End - - . += "*---------*" - if(print_flavor_text()) . += "\n[print_flavor_text()]\n" - + // VOREStation Edit: End + . += "*---------*" if (pose) if(!findtext(pose, regex("\[.?!]$"))) // Will be zero if the last character is not a member of [.?!] pose = addtext(pose,".") //Makes sure all emotes end with a period. . += "
It is [pose]" //Extra
intentional - \ No newline at end of file diff --git a/code/modules/mob/living/silicon/pai/life.dm b/code/modules/mob/living/silicon/pai/life.dm index 4879b7460c2..2871988eee8 100644 --- a/code/modules/mob/living/silicon/pai/life.dm +++ b/code/modules/mob/living/silicon/pai/life.dm @@ -1,6 +1,6 @@ /mob/living/silicon/pai/Life() - if (src.stat == 2) + if (src.stat == DEAD) return if(src.cable) diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm index 9c827839287..e007e0aa44f 100644 --- a/code/modules/mob/living/silicon/pai/pai.dm +++ b/code/modules/mob/living/silicon/pai/pai.dm @@ -50,7 +50,9 @@ "Panther" = "panther", "Cyber Elf" = "cyberelf", "Teppi" = "teppi", - "Catslug" = "catslug" + "Catslug" = "catslug", + "Car" = "car", + "Type One" = "typeone" //VOREStation Addition End ) @@ -286,6 +288,7 @@ if(istype(T)) T.visible_message("[src] folds outwards, expanding into a mobile form.") verbs |= /mob/living/silicon/pai/proc/pai_nom verbs |= /mob/living/proc/vertical_nom + update_icon() /mob/living/silicon/pai/verb/fold_up() set category = "pAI Commands" diff --git a/code/modules/mob/living/silicon/pai/pai_hud.dm b/code/modules/mob/living/silicon/pai/pai_hud.dm new file mode 100644 index 00000000000..6bdea250d24 --- /dev/null +++ b/code/modules/mob/living/silicon/pai/pai_hud.dm @@ -0,0 +1,263 @@ +/mob/living/silicon/pai + var/obj/screen/pai/pai_fold_display = null + +/obj/screen/pai/pai_fold_display + name = "fold/unfold" + icon = 'icons/mob/pai_hud.dmi' + +/datum/hud + var/list/hud_elements = list() + +/mob/living/silicon/pai/create_mob_hud(datum/hud/HUD) + ..() + + var/ui_style = 'icons/mob/pai_hud.dmi' + + var/ui_color = "#ffffff" + var/ui_alpha = 255 + + var/list/adding = list() + var/list/other = list() + var/list/hotkeybuttons = list() + var/list/hud_elements = list() + + HUD.adding = adding + HUD.other = other + HUD.hotkeybuttons = hotkeybuttons + HUD.hud_elements = hud_elements + + var/obj/screen/using + + //Small intent quarters + + using = new /obj/screen() + using.name = I_HELP + using.icon = ui_style + using.icon_state = "intent_help-s" + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM //These sit on the intent box + HUD.adding += using + HUD.help_intent = using + + using = new /obj/screen() + using.name = I_DISARM + using.icon = ui_style + using.icon_state = "intent_disarm-n" + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM + HUD.adding += using + HUD.disarm_intent = using + + using = new /obj/screen() + using.name = I_GRAB + using.icon = ui_style + using.icon_state = "intent_grab-n" + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM + HUD.adding += using + HUD.grab_intent = using + + using = new /obj/screen() + using.name = I_HURT + using.icon = ui_style + using.icon_state = "intent_harm-n" + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM + HUD.adding += using + HUD.hurt_intent = using + + //Move intent (walk/run) + using = new /obj/screen() + using.name = "mov_intent" + using.icon = ui_style + using.icon_state = (m_intent == "run" ? "running" : "walking") + using.screen_loc = ui_movi + using.color = ui_color + using.alpha = ui_alpha + HUD.adding += using + HUD.move_intent = using + + //Resist button + using = new /obj/screen() + using.name = "resist" + using.icon = ui_style + using.icon_state = "act_resist" + using.screen_loc = ui_movi + using.color = ui_color + using.alpha = ui_alpha + HUD.hotkeybuttons += using + + //Choose chassis button + using = new /obj/screen() + using.name = "choose chassis" + using.icon = ui_style + using.icon_state = "choose_chassis" + using.screen_loc = ui_movi + using.color = ui_color + using.alpha = ui_alpha + hud_elements |= using + + //Software interface button + using = new /obj/screen() + using.name = "software interface" + using.icon = ui_style + using.icon_state = "software_interface" + using.screen_loc = ui_acti + using.color = ui_color + using.alpha = ui_alpha + hud_elements |= using + + //Radio configuration button + using = new /obj/screen() + using.name = "radio configuration" + using.icon = ui_style + using.icon_state = "radio_configuration" + using.screen_loc = ui_acti + using.color = ui_color + using.alpha = ui_alpha + hud_elements |= using + + //PDA button + using = new /obj/screen() + using.name = "pda" + using.icon = ui_style + using.icon_state = "pda" + using.screen_loc = ui_pai_comms + using.color = ui_color + using.alpha = ui_alpha + hud_elements |= using + + //Communicator button + using = new /obj/screen() + using.name = "communicator" + using.icon = ui_style + using.icon_state = "communicator" + using.screen_loc = ui_pai_comms + using.color = ui_color + using.alpha = ui_alpha + hud_elements |= using + + //Language button + using = new /obj/screen() + using.name = "known languages" + using.icon = ui_style + using.icon_state = "language" + using.screen_loc = ui_acti + using.color = ui_color + using.alpha = ui_alpha + hud_elements |= using + + //Pull button + pullin = new /obj/screen() + pullin.icon = ui_style + pullin.icon_state = "pull0" + pullin.name = "pull" + pullin.screen_loc = ui_movi + HUD.hotkeybuttons += pullin + HUD.hud_elements |= pullin + + //Health status + healths = new /obj/screen() + healths.icon = ui_style + healths.icon_state = "health0" + healths.name = "health" + healths.screen_loc = ui_health + HUD.hud_elements |= healths + + pain = new /obj/screen( null ) + + zone_sel = new /obj/screen/zone_sel( null ) + zone_sel.icon = ui_style + zone_sel.color = ui_color + zone_sel.alpha = ui_alpha + zone_sel.cut_overlays() + zone_sel.update_icon() + HUD.hud_elements |= zone_sel + + pai_fold_display = new /obj/screen/pai/pai_fold_display() + pai_fold_display.screen_loc = ui_health + pai_fold_display.icon_state = "folded" + HUD.hud_elements |= pai_fold_display + + + if(client) + client.screen = list() + client.screen += hud_elements + client.screen += adding + hotkeybuttons + client.screen += client.void + +/mob/living/silicon/pai/handle_regular_hud_updates() + . = ..() + if(healths) + if(stat != DEAD) + var/heal_per = (health / getMaxHealth()) * 100 + switch(heal_per) + if(100 to INFINITY) + healths.icon_state = "health0" + if(80 to 100) + healths.icon_state = "health1" + if(60 to 80) + healths.icon_state = "health2" + if(40 to 60) + healths.icon_state = "health3" + if(20 to 40) + healths.icon_state = "health4" + if(0 to 20) + healths.icon_state = "health5" + else + healths.icon_state = "health6" + else + healths.icon_state = "health7" + + if(pai_fold_display) + if(loc == card) + pai_fold_display.icon_state = "folded" + else + pai_fold_display.icon_state = "unfolded" + +/mob/living/silicon/pai/toggle_hud_vis(full) + + if(hud_used.hud_shown) + hud_used.hud_shown = 0 + if(hud_used.adding) + client.screen -= hud_used.adding + if(hud_used.other) + client.screen -= hud_used.other + if(hud_used.hotkeybuttons) + client.screen -= hud_used.hotkeybuttons + if(hud_used.other_important) + client.screen -= hud_used.other_important + if(hud_used.hud_elements) + client.screen -= hud_used.hud_elements + + else + hud_used.hud_shown = 1 + if(hud_used.adding) + client.screen += hud_used.adding + if(hud_used.other && hud_used.inventory_shown) + client.screen += hud_used.other + if(hud_used.other_important) + client.screen += hud_used.other_important + if(hud_used.hotkeybuttons && !hud_used.hotkey_ui_hidden) + client.screen += hud_used.hotkeybuttons + if(healths) + client.screen |= healths + if(internals) + client.screen |= internals + if(gun_setting_icon) + client.screen |= gun_setting_icon + if(hud_used.hud_elements) + client.screen |= hud_used.hud_elements + + hud_used?.action_intent.screen_loc = ui_acti //Restore intent selection to the original position + client.screen += zone_sel //This one is a special snowflake + + hud_used.hidden_inventory_update() + hud_used.persistant_inventory_update() + update_action_buttons() + hud_used.reorganize_alerts() + return TRUE diff --git a/code/modules/mob/living/silicon/pai/pai_vr.dm b/code/modules/mob/living/silicon/pai/pai_vr.dm index f8eef7b8fea..61e2f10a211 100644 --- a/code/modules/mob/living/silicon/pai/pai_vr.dm +++ b/code/modules/mob/living/silicon/pai/pai_vr.dm @@ -38,7 +38,9 @@ "pai-fen", "cyberelf", "teppi", - "catslug" + "catslug", + "car", + "typeone" ) /mob/living/silicon/pai/Initialize() @@ -195,22 +197,169 @@ if(!ismob(A) || A == src) return + switch(a_intent) + if(I_HELP) + if(isliving(A)) + hug(src, A) + if(I_GRAB) + pai_nom(A) + +/mob/living/silicon/pai/proc/hug(var/mob/living/silicon/pai/H, var/mob/living/target) + var/t_him = "them" - if(ishuman(A)) - var/mob/living/carbon/human/T = A - if(!T.species.ambiguous_genders) - switch(T.identifying_gender) - if(MALE) - t_him = "him" - if(FEMALE) - t_him = "her" - else - t_him = "them" - else - switch(A.gender) + if(ishuman(target)) + var/mob/living/carbon/human/T = target + switch(T.identifying_gender) if(MALE) t_him = "him" if(FEMALE) t_him = "her" - visible_message("\The [src] hugs [A] to make [t_him] feel better!", \ - "You hug [A] to make [t_him] feel better!") \ No newline at end of file + if(NEUTER) + t_him = "it" + if(HERM) + t_him = "hir" + else + t_him = "them" + else + switch(target.gender) + if(MALE) + t_him = "him" + if(FEMALE) + t_him = "her" + if(NEUTER) + t_him = "it" + if(HERM) + t_him = "hir" + else + t_him = "them" + + if(H.zone_sel.selecting == "head") + H.visible_message( \ + "[H] pats [target] on the head.", \ + "You pat [target] on the head.", ) + else if(H.zone_sel.selecting == "r_hand" || H.zone_sel.selecting == "l_hand") + H.visible_message( \ + "[H] shakes [target]'s hand.", \ + "You shake [target]'s hand.", ) + else if(H.zone_sel.selecting == "mouth") + H.visible_message( \ + "[H] boops [target]'s nose.", \ + "You boop [target] on the nose.", ) + else + H.visible_message("[H] hugs [target] to make [t_him] feel better!", \ + "You hug [target] to make [t_him] feel better!") + playsound(src, 'sound/weapons/thudswoosh.ogg', 50, 1, -1) + +/mob/living/silicon/pai/proc/savefile_path(mob/user) + return "data/player_saves/[copytext(user.ckey, 1, 2)]/[user.ckey]/pai.sav" + +/mob/living/silicon/pai/proc/savefile_save(mob/user) + if(IsGuestKey(user.key)) + return 0 + + var/savefile/F = new /savefile(src.savefile_path(user)) + + + F["name"] << src.name + F["description"] << src.flavor_text + F["eyecolor"] << src.eye_color + F["chassis"] << src.chassis + F["emotion"] << src.card.current_emotion + F["gender"] << src.gender + F["version"] << 1 + + return 1 + +/mob/living/silicon/pai/proc/savefile_load(mob/user, var/silent = 1) + if (IsGuestKey(user.key)) + return 0 + + var/path = savefile_path(user) + + if (!fexists(path)) + return 0 + + var/savefile/F = new /savefile(path) + + if(!F) return //Not everyone has a pai savefile. + + var/version = null + F["version"] >> version + + if (isnull(version) || version != 1) + fdel(path) + if (!silent) + tgui_alert_async(user, "Your savefile was incompatible with this version and was deleted.") + return 0 + var/ourname + var/ouremotion + var/ourdesc + var/oureyes + var/ourchassis + var/ourgender + F["name"] >> ourname + F["description"] >> ourdesc + F["eyecolor"] >> oureyes + F["chassis"] >> ourchassis + F["emotion"] >> ouremotion + F["gender"] >> ourgender + if(ourname) + SetName(ourname) + if(ourdesc) + flavor_text = ourdesc + if(ourchassis) + chassis = ourchassis + if(ourgender) + gender = ourgender + if(oureyes) + card.screen_color = oureyes + eye_color = oureyes + if(ouremotion) + card.setEmotion(ouremotion) + + update_icon() + return 1 + +/mob/living/silicon/pai/verb/save_pai_to_slot() + set category = "pAI Commands" + set name = "Save Configuration" + savefile_save(src) + to_chat(src, "[name] configuration saved to global pAI settings.") + +/mob/living/silicon/pai/a_intent_change(input as text) + . = ..() + + switch(a_intent) + if(I_HELP) + hud_used.help_intent.icon_state = "intent_help-s" + hud_used.disarm_intent.icon_state = "intent_disarm-n" + hud_used.grab_intent.icon_state = "intent_grab-n" + hud_used.hurt_intent.icon_state = "intent_harm-n" + + if(I_DISARM) + hud_used.help_intent.icon_state = "intent_help-n" + hud_used.disarm_intent.icon_state = "intent_disarm-s" + hud_used.grab_intent.icon_state = "intent_grab-n" + hud_used.hurt_intent.icon_state = "intent_harm-n" + + if(I_GRAB) + hud_used.help_intent.icon_state = "intent_help-n" + hud_used.disarm_intent.icon_state = "intent_disarm-n" + hud_used.grab_intent.icon_state = "intent_grab-s" + hud_used.hurt_intent.icon_state = "intent_harm-n" + + if(I_HURT) + hud_used.help_intent.icon_state = "intent_help-n" + hud_used.disarm_intent.icon_state = "intent_disarm-n" + hud_used.grab_intent.icon_state = "intent_grab-n" + hud_used.hurt_intent.icon_state = "intent_harm-s" + +/mob/living/silicon/pai/verb/toggle_gender_identity_vr() + set name = "Set Gender Identity" + set desc = "Sets the pronouns when examined and performing an emote." + set category = "IC" + var/new_gender_identity = tgui_input_list(usr, "Please select a gender Identity:", "Set Gender Identity", list(FEMALE, MALE, NEUTER, PLURAL, HERM)) + if(!new_gender_identity) + return 0 + gender = new_gender_identity + return 1 diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/mob/living/silicon/pai/personality.dm index 73ae49dcb78..de2b8fa2021 100644 --- a/code/modules/mob/living/silicon/pai/personality.dm +++ b/code/modules/mob/living/silicon/pai/personality.dm @@ -57,4 +57,8 @@ F["description"] >> src.description F["role"] >> src.role F["comments"] >> src.comments + F["eyecolor"] >> src.eye_color + F["chassis"] >> src.chassis + F["emotion"] >> src.ouremotion + return 1 diff --git a/code/modules/mob/living/silicon/pai/recruit.dm b/code/modules/mob/living/silicon/pai/recruit.dm index ffd817d9c48..a1dd02b1635 100644 --- a/code/modules/mob/living/silicon/pai/recruit.dm +++ b/code/modules/mob/living/silicon/pai/recruit.dm @@ -11,7 +11,10 @@ var/datum/paiController/paiController // Global handler for pAI candidates var/role var/comments var/ready = 0 - + var/chassis + var/ouremotion + var/eye_color + var/gender /hook/startup/proc/paiControllerSetup() paiController = new /datum/paiController() @@ -33,14 +36,25 @@ var/datum/paiController/paiController // Global handler for pAI candidates return if(istype(card,/obj/item/device/paicard) && istype(candidate,/datum/paiCandidate)) var/mob/living/silicon/pai/pai = new(card) - if(!candidate.name) - pai.name = pick(ninja_names) - else - pai.name = candidate.name - pai.real_name = pai.name pai.key = candidate.key - card.setPersonality(pai) + if(!candidate.name) + pai.SetName(pick(ninja_names)) + else + pai.SetName(candidate.name) + if(candidate.description) + pai.flavor_text = candidate.description + if(candidate.eye_color) + pai.eye_color = candidate.eye_color + card.screen_color = pai.eye_color + if(candidate.chassis) + pai.chassis = candidate.chassis + if(candidate.ouremotion) + card.setEmotion(candidate.ouremotion) + if(candidate.gender) + pai.gender = candidate.gender + pai.update_icon() + pai.real_name = pai.name card.looking_for_personality = 0 if(pai.mind) update_antag_icons(pai.mind) @@ -210,7 +224,7 @@ var/datum/paiController/paiController // Global handler for pAI candidates - Load Personality + Load Personality
diff --git a/code/modules/mob/living/silicon/pai/software_modules.dm b/code/modules/mob/living/silicon/pai/software_modules.dm index 7e4bff120a3..07d82b74d0a 100644 --- a/code/modules/mob/living/silicon/pai/software_modules.dm +++ b/code/modules/mob/living/silicon/pai/software_modules.dm @@ -98,9 +98,10 @@ /datum/pai_software/crew_manifest name = "Crew Manifest" - ram_cost = 5 + ram_cost = 0 id = "manifest" toggle = 0 + default = 1 //Comes with the communicator already, also why not /datum/pai_software/crew_manifest/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui) ui = SStgui.try_update_ui(user, src, ui) @@ -117,9 +118,10 @@ /datum/pai_software/messenger name = "Digital Messenger" - ram_cost = 5 + ram_cost = 0 id = "messenger" toggle = 0 + default = 1 //Can already be accessed through verbs, and also why not /datum/pai_software/messenger/tgui_interact(mob/living/silicon/pai/user, datum/tgui/ui, datum/tgui/parent_ui) return user.pda.tgui_interact(user, parent_ui = parent_ui) diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/pets/fennec.dm b/code/modules/mob/living/simple_mob/subtypes/animal/pets/fennec.dm new file mode 100644 index 00000000000..0ea451246ff --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/pets/fennec.dm @@ -0,0 +1,29 @@ +/mob/living/simple_mob/animal/passive/fennec + name = "fennec" + desc = "A fox preferring arid climates, also known as a dingler, or a goob." + tt_desc = "Vulpes Zerda" + icon_state = "fennec" + item_state = "fennec" + + movement_cooldown = 0.5 SECONDS + + see_in_dark = 6 + response_help = "pets" + response_disarm = "gently pushes aside" + response_harm = "kicks" + + holder_type = /obj/item/holder/fennec + mob_size = MOB_SMALL + + has_langs = list("Cat, Dog") //they're similar, why not. + +/mob/living/simple_mob/animal/passive/fennec/faux + name = "faux" + desc = "Domesticated fennec. Seems to like screaming just as much though." + +/mob/living/simple_mob/animal/passive/fennec/Initialize() + icon_living = "[initial(icon_state)]" + icon_dead = "[initial(icon_state)]_dead" + icon_rest = "[initial(icon_state)]_rest" + update_icon() + return ..() diff --git a/code/modules/mob/new_player/preferences_setup.dm b/code/modules/mob/new_player/preferences_setup.dm index 66d712bc449..e564bba7b01 100644 --- a/code/modules/mob/new_player/preferences_setup.dm +++ b/code/modules/mob/new_player/preferences_setup.dm @@ -259,6 +259,7 @@ mannequin.update_transform() //VOREStation Edit to update size/shape stuff. mannequin.toggle_tail(setting = TRUE) mannequin.toggle_wing(setting = TRUE) + mannequin.update_tail_showing() COMPILE_OVERLAYS(mannequin) update_character_previews(new /mutable_appearance(mannequin)) diff --git a/code/modules/mob/new_player/sprite_accessories_ear_vr.dm b/code/modules/mob/new_player/sprite_accessories_ear_vr.dm index f6b0c77abeb..4c09f5d7cd1 100644 --- a/code/modules/mob/new_player/sprite_accessories_ear_vr.dm +++ b/code/modules/mob/new_player/sprite_accessories_ear_vr.dm @@ -119,6 +119,15 @@ do_colouration = 1 color_blend_mode = ICON_MULTIPLY +/datum/sprite_accessory/ears/antennae_eye + name = "antennae eye, colorable" + desc = "" + icon_state = "antennae" + extra_overlay = "antennae_eye_1" + extra_overlay2 = "antennae_eye_2" + do_colouration = 1 + color_blend_mode = ICON_MULTIPLY + /datum/sprite_accessory/ears/curly_bug name = "curly antennae, colorable" desc = "" diff --git a/code/modules/mob/new_player/sprite_accessories_taur.dm b/code/modules/mob/new_player/sprite_accessories_taur.dm index 4637ac008e1..4288f9bbc6d 100644 --- a/code/modules/mob/new_player/sprite_accessories_taur.dm +++ b/code/modules/mob/new_player/sprite_accessories_taur.dm @@ -285,6 +285,11 @@ suit_sprites = 'icons/mob/taursuits_slug.dmi' icon_sprite_tag = "slug" +/datum/sprite_accessory/tail/taur/slug/snail + name = "Snail (Taur)" + icon_state = "slug_s" + extra_overlay = "snail_shell_marking" + /datum/sprite_accessory/tail/taur/frog name = "Frog (Taur)" icon_state = "frog_s" diff --git a/code/modules/mob/new_player/sprite_accessories_taur_vr.dm b/code/modules/mob/new_player/sprite_accessories_taur_vr.dm index f182b5792e9..2a574510b04 100644 --- a/code/modules/mob/new_player/sprite_accessories_taur_vr.dm +++ b/code/modules/mob/new_player/sprite_accessories_taur_vr.dm @@ -69,6 +69,15 @@ extra_overlay2 = "wolf_markings_2" //icon_sprite_tag = "wolf2c" +/datum/sprite_accessory/tail/taur/wolf/wolf_2c_wag + name = "Wolf 3-color (Taur, vwag)" + icon_state = "wolf_s" + extra_overlay = "wolf_markings" + extra_overlay2 = "wolf_markings_2" + ani_state = "fatwolf_s" + extra_overlay_w = "fatwolf_markings" + extra_overlay2_w = "wolf_markings_2" + /datum/sprite_accessory/tail/taur/wolf/fatwolf_2c name = "Fat Wolf 3-color (Taur)" icon_state = "fatwolf_s" @@ -411,6 +420,12 @@ suit_sprites = 'icons/mob/taursuits_drake_vr.dmi' icon_sprite_tag = "drake" +/datum/sprite_accessory/tail/taur/ch/fatdrake + name = "Drake (Fat Taur dual-color)" + icon_state = "fatdrake_s" + extra_overlay = "fatdrake_markings" + icon_sprite_tag = "drake" + /datum/sprite_accessory/tail/taur/otie name = "Otie (Taur)" icon_state = "otie_s" diff --git a/code/modules/nifsoft/software/14_commlink.dm b/code/modules/nifsoft/software/14_commlink.dm index 93e1af799c3..75a04fc5894 100644 --- a/code/modules/nifsoft/software/14_commlink.dm +++ b/code/modules/nifsoft/software/14_commlink.dm @@ -128,4 +128,4 @@ return if(ringer && nif.human) - nif.notify("Commlink message from [who]: \"[text]\" (Open)") + nif.notify("Commlink message from [who]: \"[text]\" (Open) (Reply)") diff --git a/code/modules/tgui/modules/rcon.dm b/code/modules/tgui/modules/rcon.dm index 01ba355248f..0a29153d76c 100644 --- a/code/modules/tgui/modules/rcon.dm +++ b/code/modules/tgui/modules/rcon.dm @@ -1,3 +1,5 @@ +#define SMES_PER_PAGE 4 + /datum/tgui_module/rcon name = "Power RCON" tgui_id = "RCON" @@ -5,17 +7,41 @@ var/list/known_SMESs = null var/list/known_breakers = null + var/filtered_smeslist = list() + + var/current_page = 1 + var/number_pages = 0 + +/datum/tgui_module/rcon/proc/filter_smeslist(var/page) + number_pages = known_SMESs.len / SMES_PER_PAGE + + if(number_pages != round(number_pages)) + number_pages = round(number_pages) + 1 + var/page_index = page - 1 + + var/lower_bound = page_index * SMES_PER_PAGE + 1 + var/upper_bound = (page_index + 1) * SMES_PER_PAGE + upper_bound = min(upper_bound, known_SMESs.len) + filtered_smeslist = list() + + for(var/index = lower_bound, index <= upper_bound, index++) + filtered_smeslist += known_SMESs[index] + /datum/tgui_module/rcon/tgui_data(mob/user) FindDevices() // Update our devices list var/list/data = ..() + filter_smeslist(current_page) + // SMES DATA (simplified view) var/list/smeslist[0] - for(var/obj/machinery/power/smes/buildable/SMES in known_SMESs) + for(var/obj/machinery/power/smes/buildable/SMES in filtered_smeslist) var/list/smes_data = SMES.tgui_data() smes_data["RCON_tag"] = SMES.RCon_tag smeslist.Add(list(smes_data)) + data["pages"] = number_pages + 1 + data["current_page"] = current_page data["smes_info"] = sortByKey(smeslist, "RCON_tag") // BREAKER DATA (simplified view) @@ -34,6 +60,10 @@ return TRUE switch(action) + if("set_smes_page") + var/page = params["index"] + current_page = page + . = TRUE if("smes_in_toggle") var/obj/machinery/power/smes/buildable/SMES = GetSMESByTag(params["smes"]) if(SMES) diff --git a/code/modules/vchat/vchat_client.dm b/code/modules/vchat/vchat_client.dm index a918c94cde5..7520b41abb0 100644 --- a/code/modules/vchat/vchat_client.dm +++ b/code/modules/vchat/vchat_client.dm @@ -161,6 +161,7 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic update_vis() spawn() + if(owner.is_preference_enabled(/datum/client_preference/vchat_enable)) tgui_alert_async(owner,"VChat didn't load after some time. Switching to use oldchat as a fallback. Try using 'Reload VChat' verb in OOC verbs, or reconnecting to try again.") //Provide the JS with who we are diff --git a/code/modules/vore/eating/belly_obj_vr.dm b/code/modules/vore/eating/belly_obj_vr.dm index 4054caa54e6..6ebda2446b9 100644 --- a/code/modules/vore/eating/belly_obj_vr.dm +++ b/code/modules/vore/eating/belly_obj_vr.dm @@ -342,7 +342,7 @@ owner.update_icons() //Print notifications/sound if necessary - if(!silent) + if(!silent && count) owner.visible_message("[owner] expels everything from their [lowertext(name)]!") var/soundfile if(!fancy_vore) @@ -378,6 +378,9 @@ slip.slip_protect = world.time + 25 // This is to prevent slipping back into your pred if they stand on soap or something. //Place them into our drop_location M.forceMove(drop_location()) + if(ismob(M)) + var/mob/ourmob = M + ourmob.reset_view(null) items_preserved -= M //Special treatment for absorbed prey @@ -416,6 +419,10 @@ soundfile = fancy_release_sounds[release_sound] if(soundfile) playsound(src, soundfile, vol = 100, vary = 1, falloff = VORE_SOUND_FALLOFF, preference = /datum/client_preference/eating_noises, volume_channel = VOLUME_CHANNEL_VORE) + //Should fix your view not following you out of mobs sometimes! + if(ismob(M)) + var/mob/ourmob = M + ourmob.reset_view(null) return 1 @@ -429,6 +436,9 @@ prey.buckled.unbuckle_mob() prey.forceMove(src) + if(ismob(prey)) + var/mob/ourmob = prey + ourmob.reset_view(owner) owner.updateVRPanel() if(isanimal(owner)) owner.update_icon() @@ -723,6 +733,10 @@ if(M.loc != src) M.forceMove(src) + if(ismob(M)) + var/mob/ourmob = M + ourmob.reset_view(owner) + //Seek out absorbed prey of the prey, absorb them too. //This in particular will recurse oddly because if there is absorbed prey of prey of prey... //it will just move them up one belly. This should never happen though since... when they were @@ -1033,6 +1047,9 @@ if(!(content in src) || !istype(target)) return content.forceMove(target) + if(ismob(content)) + var/mob/ourmob = content + ourmob.reset_view(owner) if(isitem(content)) var/obj/item/I = content if(istype(I,/obj/item/weapon/card/id)) diff --git a/code/modules/vore/eating/living_vr.dm b/code/modules/vore/eating/living_vr.dm index ca2fcf3fa4c..d0acc9614f8 100644 --- a/code/modules/vore/eating/living_vr.dm +++ b/code/modules/vore/eating/living_vr.dm @@ -1062,9 +1062,13 @@ if(!screen_icon) screen_icon = new() RegisterSignal(screen_icon, COMSIG_CLICK, .proc/vore_panel_click) - screen_icon.icon = HUD.ui_style - screen_icon.color = HUD.ui_color - screen_icon.alpha = HUD.ui_alpha + if(ispAI(user)) + screen_icon.icon = 'icons/mob/pai_hud.dmi' + screen_icon.screen_loc = ui_acti + else + screen_icon.icon = HUD.ui_style + screen_icon.color = HUD.ui_color + screen_icon.alpha = HUD.ui_alpha LAZYADD(HUD.other_important, screen_icon) user.client?.screen += screen_icon diff --git a/code/modules/vore/resizing/resize_vr.dm b/code/modules/vore/resizing/resize_vr.dm index b82ecb07e74..dd131ffca34 100644 --- a/code/modules/vore/resizing/resize_vr.dm +++ b/code/modules/vore/resizing/resize_vr.dm @@ -113,8 +113,6 @@ /mob/living/carbon/human/resize(var/new_size, var/animate = TRUE, var/uncapped = FALSE, var/ignore_prefs = FALSE, var/aura_animation = TRUE) if(!resizable && !ignore_prefs) return 1 - if(species) - vis_height = species.icon_height . = ..() if(LAZYLEN(hud_list) && has_huds) var/new_y_offset = vis_height * (size_multiplier - 1) @@ -147,8 +145,8 @@ resize(new_size/100, uncapped = has_large_resize_bounds(), ignore_prefs = TRUE) // I'm not entirely convinced that `src ? ADMIN_JMP(src) : "null"` here does anything // but just in case it does, I'm leaving the null-src checking - message_admins("[key_name(src)] used the resize command in-game to be [new_size]% size. [src ? ADMIN_JMP(src) : "null"]") - + log_admin("[key_name(src)] used the resize command in-game to be [new_size]% size. [src ? ADMIN_JMP(src) : "null"]") + /* //Add the set_size() proc to usable verbs. By commenting this out, we can leave the proc and hand it to species that need it. /hook/living_new/proc/resize_setup(mob/living/H) diff --git a/icons/_nanomaps/gb1.png b/icons/_nanomaps/gb1.png new file mode 100644 index 00000000000..8c3ac8f2db8 Binary files /dev/null and b/icons/_nanomaps/gb1.png differ diff --git a/icons/_nanomaps/gb2.png b/icons/_nanomaps/gb2.png new file mode 100644 index 00000000000..63277ea186a Binary files /dev/null and b/icons/_nanomaps/gb2.png differ diff --git a/icons/_nanomaps/gb3.png b/icons/_nanomaps/gb3.png new file mode 100644 index 00000000000..2ad403f15e8 Binary files /dev/null and b/icons/_nanomaps/gb3.png differ diff --git a/icons/_nanomaps/gbeast.png b/icons/_nanomaps/gbeast.png new file mode 100644 index 00000000000..2f6f110f427 Binary files /dev/null and b/icons/_nanomaps/gbeast.png differ diff --git a/icons/_nanomaps/gbmining.png b/icons/_nanomaps/gbmining.png new file mode 100644 index 00000000000..769d080980a Binary files /dev/null and b/icons/_nanomaps/gbmining.png differ diff --git a/icons/_nanomaps/gbnorth.png b/icons/_nanomaps/gbnorth.png new file mode 100644 index 00000000000..5b4f03662e7 Binary files /dev/null and b/icons/_nanomaps/gbnorth.png differ diff --git a/icons/_nanomaps/gbsouth.png b/icons/_nanomaps/gbsouth.png new file mode 100644 index 00000000000..1a3a1b22e39 Binary files /dev/null and b/icons/_nanomaps/gbsouth.png differ diff --git a/icons/_nanomaps/gbwest.png b/icons/_nanomaps/gbwest.png new file mode 100644 index 00000000000..d19c3019a8c Binary files /dev/null and b/icons/_nanomaps/gbwest.png differ diff --git a/icons/inventory/head/mob_vox.dmi b/icons/inventory/head/mob_vox.dmi index 346ea9b7d18..f1570089c8a 100644 Binary files a/icons/inventory/head/mob_vox.dmi and b/icons/inventory/head/mob_vox.dmi differ diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index def737dcc36..2cb35cf667e 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi new file mode 100644 index 00000000000..3e8d60a5798 Binary files /dev/null and b/icons/mob/head.dmi differ diff --git a/icons/mob/pai_hud.dmi b/icons/mob/pai_hud.dmi new file mode 100644 index 00000000000..c1be3ab314d Binary files /dev/null and b/icons/mob/pai_hud.dmi differ diff --git a/icons/mob/pai_vr.dmi b/icons/mob/pai_vr.dmi index 705369c7f30..859cfc71162 100644 Binary files a/icons/mob/pai_vr.dmi and b/icons/mob/pai_vr.dmi differ diff --git a/icons/mob/species/teshari/head.dmi b/icons/mob/species/teshari/head.dmi index 0bb0637c4ee..6cf6b2ac7de 100644 Binary files a/icons/mob/species/teshari/head.dmi and b/icons/mob/species/teshari/head.dmi differ diff --git a/icons/mob/taursuits_horse.dmi b/icons/mob/taursuits_horse.dmi index 192f282796b..4986bed14f3 100644 Binary files a/icons/mob/taursuits_horse.dmi and b/icons/mob/taursuits_horse.dmi differ diff --git a/icons/mob/taursuits_horse_vr.dmi b/icons/mob/taursuits_horse_vr.dmi index 192f282796b..4986bed14f3 100644 Binary files a/icons/mob/taursuits_horse_vr.dmi and b/icons/mob/taursuits_horse_vr.dmi differ diff --git a/icons/mob/vore/ears_vr.dmi b/icons/mob/vore/ears_vr.dmi index 032ba8e4fa0..4bde02b8bf3 100644 Binary files a/icons/mob/vore/ears_vr.dmi and b/icons/mob/vore/ears_vr.dmi differ diff --git a/icons/mob/vore/taurs_vr.dmi b/icons/mob/vore/taurs_vr.dmi index d10b18cb041..20ddaee2a40 100644 Binary files a/icons/mob/vore/taurs_vr.dmi and b/icons/mob/vore/taurs_vr.dmi differ diff --git a/icons/obj/pda.dmi b/icons/obj/pda.dmi index e7e7f0859cf..f98d60b5bdf 100644 Binary files a/icons/obj/pda.dmi and b/icons/obj/pda.dmi differ diff --git a/maps/expedition_vr/beach/beach.dmm b/maps/expedition_vr/beach/beach.dmm index 82a821cfda5..1d3c1e62337 100644 --- a/maps/expedition_vr/beach/beach.dmm +++ b/maps/expedition_vr/beach/beach.dmm @@ -873,6 +873,10 @@ "nD" = ( /turf/simulated/mineral/ignore_mapgen, /area/tether_away/beach/water) +"nP" = ( +/mob/living/simple_mob/animal/passive/fennec/faux, +/turf/simulated/floor/wood, +/area/tether_away/beach/resort/kitchen) "of" = ( /obj/structure/table/woodentable, /obj/random/drinkbottle, @@ -11048,7 +11052,7 @@ ad ad ad ad -ad +nP ad ad ad diff --git a/maps/groundbase/eastwilds/eastwilds1.dmm b/maps/groundbase/eastwilds/eastwilds1.dmm index 28b038004a4..fc499a60956 100644 --- a/maps/groundbase/eastwilds/eastwilds1.dmm +++ b/maps/groundbase/eastwilds/eastwilds1.dmm @@ -29,6 +29,12 @@ "r" = ( /turf/simulated/mineral/cave/virgo3c, /area/submap/groundbase/wilderness/east/cave) +"u" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/deep/virgo3c, +/area/submap/groundbase/wilderness/east) "w" = ( /obj/effect/map_effect/portal/master/side_a/gb_wilds/east_west{ dir = 8 @@ -38,6 +44,12 @@ "y" = ( /turf/simulated/mineral/ignore_cavegen, /area/submap/groundbase/wilderness/east) +"B" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/virgo3c, +/area/submap/groundbase/wilderness/east) "F" = ( /turf/simulated/floor/outdoors/grass/virgo3c, /area/submap/groundbase/wilderness/east/unexplored) @@ -2383,7 +2395,7 @@ U U U U -U +u U U U @@ -2653,7 +2665,7 @@ U U U U -U +u U U U @@ -2972,7 +2984,7 @@ U U U U -U +u U U U @@ -3421,7 +3433,7 @@ U U U U -U +u U U U @@ -3513,7 +3525,7 @@ U U U U -U +u U U U @@ -4779,7 +4791,7 @@ Z U U U -U +u U U U @@ -4839,7 +4851,7 @@ Z Z Z U -U +u U U U @@ -5212,7 +5224,7 @@ U U U U -U +u U U U @@ -5245,7 +5257,7 @@ Z Z U U -U +u U U U @@ -5414,7 +5426,7 @@ U U U U -U +u Z Z Z @@ -6518,7 +6530,7 @@ F V V Z -Z +B Z Z Z @@ -6908,7 +6920,7 @@ Z Z Z Z -U +u U U U @@ -6971,7 +6983,7 @@ U U U U -U +u U U Z @@ -8192,7 +8204,7 @@ U U U U -U +u U U U @@ -8245,7 +8257,7 @@ U U U U -U +u U U U @@ -9461,7 +9473,7 @@ r r Z Z -Z +B Z U U @@ -10186,7 +10198,7 @@ U U U U -U +u U U Z @@ -10322,7 +10334,7 @@ U U U U -U +u U U U @@ -10374,7 +10386,7 @@ U U U U -U +u U U U @@ -11219,7 +11231,7 @@ Z U U U -U +u U U U @@ -11373,7 +11385,7 @@ U U U U -U +u U Z Z @@ -11476,7 +11488,7 @@ U U U U -U +u U U U @@ -12337,7 +12349,7 @@ U U U U -U +u U U U @@ -12456,7 +12468,7 @@ Z Z Z U -U +u U U U @@ -12778,7 +12790,7 @@ U U U U -U +u U U U @@ -13757,7 +13769,7 @@ Z Z U U -U +u U U U @@ -13767,7 +13779,7 @@ Z Z Z Z -Z +B Z Z Z @@ -15893,7 +15905,7 @@ V V V Z -Z +B Z Z Z diff --git a/maps/groundbase/eastwilds/eastwilds2.dmm b/maps/groundbase/eastwilds/eastwilds2.dmm index fdd0dfdc349..33a58e58c2b 100644 --- a/maps/groundbase/eastwilds/eastwilds2.dmm +++ b/maps/groundbase/eastwilds/eastwilds2.dmm @@ -37,6 +37,12 @@ }, /turf/simulated/floor/water/deep/virgo3c, /area/submap/groundbase/wilderness/east) +"B" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/deep/virgo3c, +/area/submap/groundbase/wilderness/east) "F" = ( /turf/simulated/floor/outdoors/grass/virgo3c, /area/submap/groundbase/wilderness/east/unexplored) @@ -46,6 +52,12 @@ }, /turf/simulated/floor/outdoors/grass/forest/virgo3c, /area/submap/groundbase/wilderness/east) +"I" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/virgo3c, +/area/submap/groundbase/wilderness/east) "V" = ( /obj/effect/map_effect/portal/master/side_b/gb_wilds/east{ dir = 4 @@ -949,7 +961,7 @@ Z Z Z t -t +B t t Z @@ -3082,7 +3094,7 @@ l l Z Z -Z +I t t t @@ -5071,7 +5083,7 @@ Z Z Z Z -Z +I n n F @@ -5917,7 +5929,7 @@ F n Z Z -Z +I Z Z Z @@ -6912,7 +6924,7 @@ t t t t -Z +I Z Z Z @@ -7225,7 +7237,7 @@ Z t t t -t +B t t t @@ -7515,7 +7527,7 @@ t t Z Z -Z +I Z n n @@ -8044,7 +8056,7 @@ Z Z Z Z -t +B t t t @@ -8128,7 +8140,7 @@ n n Z Z -Z +I Z Z Z @@ -8341,7 +8353,7 @@ t t t t -Z +I Z Z Z @@ -8695,7 +8707,7 @@ Z Z t t -t +B t t t @@ -8904,7 +8916,7 @@ t t Z Z -Z +I Z Z Z @@ -8992,7 +9004,7 @@ t t t t -t +B Z Z Z @@ -9067,7 +9079,7 @@ n n Z Z -Z +I t Z Z @@ -9412,7 +9424,7 @@ t t t t -t +B t t t @@ -9689,7 +9701,7 @@ n Z Z Z -Z +I t t t @@ -10688,7 +10700,7 @@ Z t t t -t +B t t t @@ -11336,7 +11348,7 @@ n n n Z -Z +I Z n n @@ -11513,7 +11525,7 @@ t t t t -t +B t t t @@ -11532,7 +11544,7 @@ Z Z t t -t +B t t t @@ -12521,7 +12533,7 @@ t t t t -t +B t t t @@ -12787,7 +12799,7 @@ t t t t -t +B t t t @@ -13531,7 +13543,7 @@ t t t t -t +B t Z Z @@ -13953,7 +13965,7 @@ t t t t -t +B t t t @@ -14303,7 +14315,7 @@ Z Z Z Z -Z +I Z Z Z @@ -14620,7 +14632,7 @@ n n Z Z -Z +I Z Z Z @@ -14644,6 +14656,7 @@ Z Z Z t +B t t t @@ -14655,8 +14668,7 @@ t t t t -t -t +B t t t @@ -16027,7 +16039,7 @@ Z Z Z Z -Z +I Z Z Z @@ -16081,6 +16093,7 @@ t t t t +B t t t @@ -16090,8 +16103,7 @@ t t t t -t -t +B t t t @@ -16352,7 +16364,7 @@ Z Z t t -t +B t t t @@ -16937,7 +16949,7 @@ t t t t -t +B t t t @@ -17085,7 +17097,7 @@ t t t t -t +B Z Z Z diff --git a/maps/groundbase/gb-z1.dmm b/maps/groundbase/gb-z1.dmm index 10e7a0f4b77..dc9a91d3c3c 100644 --- a/maps/groundbase/gb-z1.dmm +++ b/maps/groundbase/gb-z1.dmm @@ -2033,6 +2033,8 @@ /obj/structure/sign/painting/library_secure{ pixel_y = -30 }, +/obj/item/weapon/reagent_containers/food/drinks/shaker, +/obj/item/weapon/reagent_containers/glass/rag, /turf/simulated/floor/lino, /area/groundbase/civilian/cafe) "fk" = ( @@ -4302,6 +4304,11 @@ }, /turf/simulated/floor/tiled/dark, /area/groundbase/security/equipment) +"kP" = ( +/obj/structure/table/glass, +/obj/random/paicard, +/turf/simulated/floor/tiled/dark, +/area/groundbase/civilian/bar) "kQ" = ( /obj/machinery/power/apc{ dir = 8; @@ -6658,8 +6665,8 @@ /obj/machinery/cell_charger{ pixel_y = 7 }, -/obj/item/weapon/cell/high, -/obj/item/weapon/cell/high, +/obj/item/weapon/cell/super, +/obj/item/weapon/cell/super, /turf/simulated/floor/tiled, /area/groundbase/cargo/mining) "qG" = ( @@ -13713,6 +13720,7 @@ /obj/structure/cable/yellow{ icon_state = "0-4" }, +/obj/random/paicard, /turf/simulated/floor/tiled/dark, /area/groundbase/security/hos) "Hw" = ( @@ -14170,6 +14178,7 @@ /obj/machinery/atmospherics/unary/vent_pump/on{ dir = 8 }, +/obj/random/paicard, /turf/simulated/floor/tiled, /area/groundbase/engineering/ce) "Iy" = ( @@ -16174,6 +16183,8 @@ pixel_x = -3; pixel_y = 26 }, +/obj/item/weapon/reagent_containers/food/drinks/shaker, +/obj/item/weapon/reagent_containers/glass/rag, /turf/simulated/floor/lino, /area/groundbase/civilian/bar) "Nh" = ( @@ -17892,6 +17903,12 @@ }, /turf/simulated/floor/outdoors/sidewalk/side/virgo3c, /area/groundbase/level1/centsquare) +"RB" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/deep/virgo3c, +/area/groundbase/level1/eastspur) "RC" = ( /obj/machinery/atmospherics/unary/vent_scrubber/on, /obj/machinery/light{ @@ -25353,7 +25370,7 @@ ht ht KR GF -GF +kP oQ uF pj @@ -39104,7 +39121,7 @@ df uH uH eW -eW +RB eW uH uH @@ -39657,7 +39674,7 @@ eW eW eW eW -eW +RB eW eW eW @@ -39688,11 +39705,11 @@ eW eW eW eW +RB eW eW eW -eW -eW +RB eW eW eW @@ -39959,13 +39976,13 @@ eW eW eW eW +RB eW eW eW eW eW -eW -eW +RB eW eW eW @@ -40236,7 +40253,7 @@ uH uH uH eW -eW +RB eW eW eW diff --git a/maps/groundbase/gb-z2.dmm b/maps/groundbase/gb-z2.dmm index 4c01e0d4c68..48d050d8cfe 100644 --- a/maps/groundbase/gb-z2.dmm +++ b/maps/groundbase/gb-z2.dmm @@ -673,6 +673,7 @@ /obj/machinery/light/small{ dir = 8 }, +/obj/random/paicard, /turf/simulated/floor/wood, /area/groundbase/command/captainq) "bQ" = ( @@ -809,11 +810,8 @@ }, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/machinery/atmospherics/pipe/simple/hidden/supply, -/obj/structure/cable/yellow{ - icon_state = "1-8" - }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "cm" = ( /obj/structure/closet/secure_closet/freezer/meat, /turf/simulated/floor/tiled/eris/cafe, @@ -869,7 +867,7 @@ dir = 8 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "ct" = ( /obj/effect/floor_decal/industrial/outline/yellow, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{ @@ -1025,7 +1023,7 @@ "cU" = ( /obj/structure/table/bench/padded, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "cV" = ( /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{ dir = 8 @@ -2672,7 +2670,7 @@ "hL" = ( /obj/machinery/light, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "hM" = ( /obj/structure/grille, /obj/structure/window/reinforced/full, @@ -2719,7 +2717,7 @@ /area/rnd/xenobiology) "hU" = ( /obj/machinery/button/remote/blast_door{ - id = "rndshutters"; + id = "robshutters"; name = "Privacy Shutter Control"; pixel_y = 30 }, @@ -2766,7 +2764,7 @@ pixel_x = 32 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "hZ" = ( /obj/structure/cable/yellow{ icon_state = "4-8" @@ -2795,7 +2793,7 @@ dir = 8 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "ic" = ( /obj/structure/extinguisher_cabinet{ dir = 8; @@ -2870,7 +2868,7 @@ icon_state = "2-4" }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "ir" = ( /obj/machinery/firealarm{ dir = 8 @@ -2958,7 +2956,7 @@ }, /obj/effect/landmark/start/intern, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "iC" = ( /turf/simulated/wall/r_wall, /area/groundbase/command/hop) @@ -3271,7 +3269,7 @@ dir = 4 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "jv" = ( /obj/machinery/appliance/cooker/grill, /turf/simulated/floor/tiled/eris/cafe, @@ -3450,7 +3448,7 @@ }, /obj/machinery/light, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "jU" = ( /obj/machinery/door/firedoor/glass, /obj/machinery/door/airlock/glass_research{ @@ -3780,7 +3778,7 @@ pixel_y = 5 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "kY" = ( /obj/machinery/power/apc{ dir = 8 @@ -3946,7 +3944,7 @@ }, /obj/effect/landmark/start/visitor, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "lv" = ( /obj/structure/closet/secure_closet/hydroponics{ req_access = list(77) @@ -4446,7 +4444,7 @@ }, /obj/random/coin/sometimes, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "mN" = ( /obj/machinery/atmospherics/pipe/simple/hidden/supply{ dir = 9 @@ -4953,7 +4951,9 @@ /turf/simulated/floor/tiled, /area/groundbase/science/xenobot) "oy" = ( -/turf/simulated/floor/outdoors/sidewalk/slab/virgo3c, +/turf/simulated/floor/outdoors/sidewalk/slab/virgo3c{ + outdoors = 0 + }, /area/groundbase/level2/se) "oA" = ( /obj/machinery/door/blast/regular{ @@ -5491,6 +5491,15 @@ /obj/machinery/atmospherics/pipe/simple/hidden/supply, /turf/simulated/floor/tiled/dark, /area/groundbase/dorms) +"qc" = ( +/obj/structure/railing/grey{ + dir = 1 + }, +/obj/structure/railing/grey{ + dir = 8 + }, +/turf/simulated/open, +/area/groundbase/civilian/bar) "qd" = ( /obj/structure/sign/nanotrasen{ pixel_x = 32; @@ -5768,10 +5777,18 @@ /turf/simulated/floor/tiled, /area/groundbase/science/xenobot) "qQ" = ( -/obj/structure/table/standard, -/obj/structure/noticeboard{ - pixel_y = -32 +/obj/structure/closet/chefcloset, +/obj/item/glass_jar, +/obj/item/device/retail_scanner/civilian, +/obj/item/weapon/soap/nanotrasen, +/obj/item/device/destTagger{ + pixel_x = 4; + pixel_y = 3 }, +/obj/item/weapon/packageWrap, +/obj/item/weapon/packageWrap, +/obj/item/weapon/packageWrap, +/obj/item/weapon/tool/wrench, /turf/simulated/floor/tiled/eris/cafe, /area/groundbase/civilian/kitchen) "qS" = ( @@ -5909,7 +5926,7 @@ /obj/structure/window/reinforced/full, /obj/machinery/door/firedoor, /turf/simulated/floor, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "rm" = ( /obj/machinery/light, /turf/simulated/floor/wood, @@ -6222,7 +6239,7 @@ }, /obj/effect/landmark/start/intern, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "se" = ( /obj/item/device/aicard, /obj/structure/table/reinforced, @@ -6266,7 +6283,7 @@ dir = 8 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "sn" = ( /obj/machinery/atmospherics/pipe/simple/hidden/supply, /obj/machinery/light{ @@ -6405,7 +6422,7 @@ dir = 5 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "sI" = ( /obj/structure/disposalpipe/sortjunction/flipped{ dir = 1; @@ -7556,7 +7573,7 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/effect/landmark/start/visitor, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "we" = ( /obj/structure/cable/yellow{ icon_state = "2-4" @@ -7663,7 +7680,7 @@ }, /obj/machinery/hologram/holopad, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "ww" = ( /obj/structure/cable/yellow{ icon_state = "1-2" @@ -7907,7 +7924,7 @@ icon_state = "1-2" }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "xo" = ( /obj/structure/table/reinforced, /obj/machinery/chemical_dispenser/biochemistry/full, @@ -8105,7 +8122,7 @@ }, /obj/machinery/alarm, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "xN" = ( /obj/machinery/door/blast/regular{ density = 0; @@ -8131,7 +8148,7 @@ /obj/structure/table/glass, /obj/machinery/atmospherics/unary/vent_scrubber/on, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "xU" = ( /obj/structure/grille, /obj/machinery/door/firedoor, @@ -8230,13 +8247,13 @@ }, /obj/effect/landmark/start/intern, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "yh" = ( /obj/structure/cable/yellow{ icon_state = "4-8" }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "yj" = ( /obj/structure/sign/directions/kitchen{ pixel_x = -32 @@ -8424,7 +8441,7 @@ pixel_x = 32 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "yJ" = ( /obj/structure/bed/chair{ dir = 4 @@ -9066,14 +9083,8 @@ "AK" = ( /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/machinery/atmospherics/pipe/simple/hidden/supply, -/obj/machinery/power/apc{ - dir = 8 - }, -/obj/structure/cable/yellow{ - icon_state = "0-2" - }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "AL" = ( /obj/effect/floor_decal/industrial/outline/yellow, /turf/simulated/floor/tiled/dark, @@ -9197,7 +9208,7 @@ }, /obj/effect/landmark/start/visitor, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "AT" = ( /turf/simulated/floor/outdoors/grass/virgo3c, /area/groundbase/science/xenobot) @@ -9485,7 +9496,7 @@ icon_state = "4-8" }, /turf/simulated/floor/bmarble, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "BU" = ( /obj/structure/cable/yellow{ icon_state = "4-8" @@ -9627,7 +9638,7 @@ icon_state = "4-8" }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Cj" = ( /obj/structure/sign/directions/stairs_down{ pixel_y = 38 @@ -9737,7 +9748,7 @@ dir = 1 }, /turf/simulated/open, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "CE" = ( /turf/simulated/floor/wood, /area/groundbase/dorms/room8) @@ -10044,7 +10055,7 @@ pixel_x = 25 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Du" = ( /turf/simulated/wall, /area/groundbase/medical/lobby) @@ -10067,7 +10078,7 @@ }, /obj/machinery/camera/network/civilian, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Dy" = ( /obj/structure/table/reinforced, /obj/machinery/requests_console{ @@ -10243,7 +10254,7 @@ dir = 1 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "DV" = ( /obj/structure/table/rack, /obj/item/weapon/tank/jetpack/oxygen, @@ -10305,13 +10316,13 @@ }, /obj/effect/landmark/start/entertainer, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Ea" = ( /obj/structure/table/bench/padded, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/machinery/atmospherics/pipe/simple/hidden/supply, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Eb" = ( /obj/machinery/atmospherics/portables_connector{ dir = 1 @@ -10828,7 +10839,7 @@ /area/groundbase/cargo/office) "FF" = ( /turf/simulated/wall, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "FI" = ( /obj/structure/fence/corner{ dir = 5 @@ -10894,7 +10905,7 @@ }, /obj/machinery/hologram/holopad, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "FU" = ( /obj/structure/table/bench/standard, /obj/effect/landmark/start/rd, @@ -11024,7 +11035,7 @@ icon_state = "1-4" }, /turf/simulated/wall, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Gp" = ( /turf/simulated/wall, /area/groundbase/level2/nw) @@ -11130,6 +11141,12 @@ /obj/machinery/atmospherics/pipe/manifold4w/visible, /turf/simulated/floor/tiled, /area/groundbase/science/xenobot/storage) +"Gz" = ( +/obj/structure/railing/grey{ + dir = 1 + }, +/turf/simulated/open, +/area/groundbase/civilian/bar) "GA" = ( /obj/structure/table/wooden_reinforced, /obj/machinery/atmospherics/unary/vent_scrubber/on{ @@ -11646,6 +11663,14 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/dorms/room7) +"HW" = ( +/obj/machinery/door/firedoor, +/obj/machinery/door/airlock/freezer{ + name = "Kitchen"; + req_access = list(28) + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "HX" = ( /obj/machinery/atmospherics/pipe/simple/hidden/supply{ dir = 4 @@ -12250,7 +12275,7 @@ dir = 8 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "JD" = ( /obj/machinery/appliance/cooker/oven, /turf/simulated/floor/tiled/eris/cafe, @@ -13092,7 +13117,7 @@ "LS" = ( /obj/structure/table/glass, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "LT" = ( /obj/machinery/light/small{ dir = 8 @@ -13113,7 +13138,7 @@ dir = 4 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "LW" = ( /obj/structure/sign/xenobio{ plane = -34 @@ -13391,7 +13416,7 @@ icon_state = "1-2" }, /turf/simulated/wall, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "ME" = ( /turf/simulated/wall, /area/groundbase/civilian/chapel/office) @@ -13684,7 +13709,7 @@ /area/groundbase/level2/ne) "Ny" = ( /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Nz" = ( /obj/structure/bed/chair/sofa/pew/right{ dir = 1 @@ -14086,7 +14111,7 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/machinery/computer/timeclock/premade/east, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "OD" = ( /obj/structure/table/reinforced, /obj/machinery/atmospherics/pipe/simple/hidden/supply, @@ -14521,7 +14546,7 @@ dir = 8 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "PT" = ( /obj/machinery/button/remote/airlock{ dir = 8; @@ -14547,7 +14572,7 @@ dir = 4 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "PV" = ( /turf/simulated/floor/wood, /area/groundbase/dorms/room2) @@ -15049,7 +15074,7 @@ dir = 1 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Rn" = ( /obj/machinery/atmospherics/binary/pump{ dir = 1; @@ -15276,7 +15301,7 @@ }, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "RV" = ( /obj/structure/cable/yellow{ icon_state = "1-8" @@ -15448,20 +15473,10 @@ /turf/simulated/floor/tiled/dark, /area/groundbase/dorms) "Ss" = ( -/obj/structure/closet/chefcloset, -/obj/item/glass_jar, -/obj/item/device/retail_scanner/civilian, -/obj/item/weapon/soap/nanotrasen, -/obj/item/device/destTagger{ - pixel_x = 4; - pixel_y = 3 +/turf/simulated/floor/outdoors/sidewalk/slab/virgo3c{ + outdoors = 0 }, -/obj/item/weapon/packageWrap, -/obj/item/weapon/packageWrap, -/obj/item/weapon/packageWrap, -/obj/item/weapon/tool/wrench, -/turf/simulated/floor/tiled/eris/cafe, -/area/groundbase/civilian/kitchen) +/area/groundbase/level2/nw) "St" = ( /obj/structure/fence/corner{ dir = 8 @@ -15963,7 +15978,7 @@ /obj/machinery/light, /obj/effect/landmark/start/entertainer, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "TO" = ( /obj/structure/bed/chair/office/dark, /obj/effect/landmark/start/cargo, @@ -15981,7 +15996,7 @@ dir = 4 }, /turf/simulated/floor/bmarble, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "TT" = ( /obj/structure/railing/grey{ dir = 8 @@ -16031,6 +16046,10 @@ }, /turf/simulated/floor/tiled, /area/rnd/xenobiology) +"Uc" = ( +/obj/structure/stairs/spawner/south, +/turf/simulated/floor/outdoors/sidewalk/slab/virgo3c, +/area/groundbase/level2/nw) "Ud" = ( /turf/simulated/floor/tiled/white, /area/groundbase/medical/lobby) @@ -16072,6 +16091,9 @@ }, /turf/simulated/floor/tiled, /area/groundbase/science/xenobot/storage) +"Ui" = ( +/turf/simulated/open, +/area/groundbase/civilian/bar) "Uj" = ( /obj/machinery/door/blast/regular{ id = "xenobiodiv4"; @@ -16094,7 +16116,7 @@ icon_state = "1-8" }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Un" = ( /obj/machinery/atmospherics/pipe/simple/hidden/cyan{ dir = 10 @@ -16158,14 +16180,14 @@ "Ux" = ( /obj/machinery/hologram/holopad, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Uy" = ( /obj/machinery/light{ dir = 4 }, /obj/random/vendorall, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Uz" = ( /obj/structure/cable/yellow{ icon_state = "4-8" @@ -16749,7 +16771,7 @@ dir = 4 }, /turf/simulated/wall, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Wd" = ( /obj/structure/railing/grey{ dir = 8 @@ -16801,7 +16823,7 @@ }, /obj/effect/landmark/start/entertainer, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Wj" = ( /turf/simulated/wall, /area/groundbase/cargo/office) @@ -16887,6 +16909,7 @@ dir = 4; pixel_x = -30 }, +/obj/random/paicard, /turf/simulated/floor/tiled, /area/groundbase/science/rd) "Wz" = ( @@ -17110,7 +17133,7 @@ /obj/structure/table/bench/padded, /obj/effect/landmark/start/visitor, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Xl" = ( /obj/machinery/atmospherics/unary/vent_scrubber/on{ dir = 8 @@ -17202,7 +17225,7 @@ pixel_x = -25 }, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "XB" = ( /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/machinery/atmospherics/pipe/manifold/hidden/supply{ @@ -17417,7 +17440,7 @@ }, /obj/machinery/firealarm, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Yg" = ( /obj/machinery/atmospherics/pipe/simple/hidden/supply{ dir = 6 @@ -17441,7 +17464,7 @@ /obj/machinery/atmospherics/pipe/simple/hidden/supply, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Ym" = ( /obj/machinery/atmospherics/pipe/manifold/hidden/scrubbers{ dir = 1 @@ -17473,7 +17496,7 @@ "Yr" = ( /obj/structure/sign/department/bar, /turf/simulated/wall, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "Ys" = ( /obj/machinery/atmospherics/unary/vent_scrubber/on{ dir = 1 @@ -17612,7 +17635,7 @@ /obj/machinery/atmospherics/pipe/simple/hidden/supply, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/tiled/dark, -/area/groundbase/civilian/bar/upper) +/area/groundbase/civilian/bar) "YS" = ( /obj/machinery/biogenerator, /turf/simulated/floor/tiled, @@ -18466,7 +18489,7 @@ UH UH UH wS -Hn +Ss Kl Kl xE @@ -18608,7 +18631,7 @@ UH UH wS wS -Hn +Ss Kl Kl xE @@ -21477,11 +21500,11 @@ il il LA bY -Jc -Jc -Jc bY bY +Jc +Jc +Jc bY bY bY @@ -21617,12 +21640,12 @@ bY bY bY bY -bY -bY -bY -bY -bY -bY +Hn +Ja +Hn +Hn +Uc +Gp bY bY bY @@ -21761,7 +21784,7 @@ Vl Vl Vl Vl -Vl +HW Vl Vl JJ @@ -21903,7 +21926,7 @@ Oz Ej ep LD -Ss +Gc Wp JO JJ @@ -23452,7 +23475,7 @@ Bb Bb Iz yh -GS +qc CD FF bY @@ -23594,8 +23617,8 @@ Ny Ny Ny yh -YO -Gl +Gz +Ui rl bY sZ @@ -28289,7 +28312,7 @@ fp to to to -VR +fp fp fp fp diff --git a/maps/groundbase/gb-z3.dmm b/maps/groundbase/gb-z3.dmm index f8b3d9e6fa8..95d87ed2be7 100644 --- a/maps/groundbase/gb-z3.dmm +++ b/maps/groundbase/gb-z3.dmm @@ -19,6 +19,12 @@ }, /turf/simulated/floor/carpet, /area/groundbase/medical/cmo) +"aN" = ( +/obj/item/weapon/stool/padded{ + dir = 4 + }, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "aQ" = ( /obj/structure/table/glass, /obj/item/weapon/folder/white_cmo, @@ -121,6 +127,12 @@ }, /turf/simulated/floor/outdoors/sidewalk/slab/virgo3c, /area/groundbase/level3/escapepad) +"cD" = ( +/obj/structure/railing{ + dir = 4 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "cE" = ( /obj/structure/bed/chair/shuttle{ dir = 8 @@ -291,6 +303,11 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/medical/uhallway) +"fn" = ( +/obj/structure/table/steel, +/obj/random/paicard, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "fu" = ( /obj/structure/table/bench/marble, /obj/effect/landmark/start/chemist, @@ -393,6 +410,11 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/medical/uhallway) +"gV" = ( +/obj/structure/railing, +/obj/structure/table/steel, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "gZ" = ( /obj/structure/cable/yellow{ icon_state = "1-4" @@ -418,6 +440,13 @@ }, /turf/simulated/floor/tiled, /area/shuttle/groundbase/exploration) +"hM" = ( +/obj/structure/railing, +/obj/structure/bed/chair/backed_red{ + dir = 4 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "hN" = ( /obj/structure/table/bench/marble, /obj/effect/landmark/start/medical, @@ -697,6 +726,16 @@ }, /turf/simulated/wall/r_wall, /area/groundbase/level3/escapepad) +"mS" = ( +/obj/structure/railing{ + dir = 1 + }, +/obj/structure/table/steel, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) +"mT" = ( +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "mU" = ( /obj/structure/cable/yellow{ icon_state = "1-4" @@ -712,6 +751,12 @@ }, /turf/simulated/floor/outdoors/sidewalk/slab/virgo3c, /area/groundbase/level3/nw) +"mY" = ( +/obj/machinery/light{ + dir = 8 + }, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "nb" = ( /obj/structure/grille, /obj/structure/window/reinforced/polarized/full{ @@ -720,6 +765,15 @@ /obj/machinery/door/firedoor, /turf/simulated/floor, /area/groundbase/medical/cmo) +"nh" = ( +/obj/structure/railing{ + dir = 8 + }, +/obj/machinery/light{ + dir = 1 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "nq" = ( /obj/structure/railing/grey{ dir = 1 @@ -766,8 +820,18 @@ dir = 1 }, /obj/machinery/light, +/obj/random/paicard, /turf/simulated/floor/wood, /area/groundbase/medical/cmo) +"nD" = ( +/obj/structure/railing{ + dir = 1 + }, +/obj/structure/bed/chair/backed_red{ + dir = 4 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "nN" = ( /obj/machinery/light, /obj/effect/landmark/start/explorer, @@ -824,6 +888,12 @@ /obj/effect/overmap/visitable/sector/virgo3c, /turf/unsimulated/wall/planetary/virgo3c, /area/groundbase/level3/sw) +"oI" = ( +/obj/machinery/light{ + dir = 1 + }, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "oK" = ( /obj/structure/grille, /obj/structure/window/reinforced/full, @@ -877,6 +947,10 @@ /obj/machinery/atmospherics/unary/vent_scrubber/on, /turf/simulated/floor/tiled/white, /area/groundbase/medical/patient4) +"pa" = ( +/obj/item/weapon/stool/padded, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "pd" = ( /obj/machinery/portable_atmospherics/canister/oxygen, /obj/machinery/alarm{ @@ -945,6 +1019,12 @@ edge_blending_priority = -1 }, /area/groundbase/exploration/shuttlepad) +"pP" = ( +/obj/machinery/light{ + dir = 4 + }, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "pY" = ( /obj/structure/sign/nanotrasen, /turf/simulated/wall, @@ -1025,6 +1105,13 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/medical/patient1) +"rr" = ( +/obj/structure/railing, +/obj/machinery/light{ + dir = 8 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "rD" = ( /obj/structure/bookcase, /obj/machinery/atmospherics/unary/vent_pump/on, @@ -1051,6 +1138,13 @@ }, /turf/simulated/floor/tiled, /area/groundbase/exploration/equipment) +"sa" = ( +/obj/structure/railing{ + dir = 8 + }, +/obj/machinery/light, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "sb" = ( /obj/machinery/power/apc{ dir = 8 @@ -1141,6 +1235,12 @@ "tZ" = ( /turf/simulated/mineral/cave/virgo3c, /area/groundbase/level3/se) +"ud" = ( +/obj/structure/bed/chair/backed_red{ + dir = 8 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "ui" = ( /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/tiled/white, @@ -1228,6 +1328,15 @@ /obj/structure/bed/chair/sofa/corp/left, /turf/simulated/floor/carpet/retro, /area/groundbase/dorms/bathroom) +"vJ" = ( +/obj/structure/railing{ + dir = 1 + }, +/obj/machinery/light{ + dir = 4 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "vV" = ( /obj/structure/bed/chair/shuttle{ dir = 8 @@ -1392,6 +1501,10 @@ /obj/machinery/atmospherics/pipe/manifold/hidden/supply, /turf/simulated/floor/tiled/white, /area/groundbase/medical/paramedic) +"yu" = ( +/obj/machinery/light, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "yy" = ( /obj/structure/dispenser{ phorontanks = 0 @@ -1529,6 +1642,9 @@ }, /turf/simulated/floor/wood, /area/groundbase/dorms/bathroom) +"AZ" = ( +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Bd" = ( /obj/effect/floor_decal/industrial/warning{ dir = 1 @@ -1591,6 +1707,12 @@ "BH" = ( /turf/simulated/wall, /area/groundbase/medical/patient3) +"BM" = ( +/obj/structure/catwalk, +/turf/simulated/floor/virgo3c{ + edge_blending_priority = -1 + }, +/area/groundbase/level3/nw/open) "Ck" = ( /obj/structure/table/woodentable, /obj/machinery/computer/security/telescreen/entertainment{ @@ -1598,6 +1720,12 @@ }, /turf/simulated/floor/carpet/retro, /area/groundbase/dorms/bathroom) +"Ct" = ( +/obj/structure/bed/chair/backed_red{ + dir = 4 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Cv" = ( /obj/structure/bed/padded, /obj/item/weapon/bedsheet/medical, @@ -1722,6 +1850,15 @@ }, /turf/simulated/floor/tiled, /area/groundbase/exploration/equipment) +"Em" = ( +/obj/structure/railing{ + dir = 1 + }, +/obj/machinery/light{ + dir = 8 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Es" = ( /obj/structure/closet/secure_closet/pathfinder{ req_access = list(62,43,67) @@ -1830,6 +1967,12 @@ "Fh" = ( /turf/simulated/floor/wood, /area/groundbase/dorms/bathroom) +"Fr" = ( +/obj/item/weapon/stool/padded{ + dir = 1 + }, +/turf/simulated/floor/wmarble, +/area/groundbase/civilian/kitchen) "Fx" = ( /obj/structure/cable/yellow{ icon_state = "4-8" @@ -1862,6 +2005,9 @@ }, /turf/simulated/floor/bmarble, /area/groundbase/civilian/pilot) +"FU" = ( +/turf/simulated/floor/lino, +/area/groundbase/civilian/kitchen) "FV" = ( /obj/item/weapon/storage/backpack/parachute{ pixel_x = 4; @@ -1912,6 +2058,13 @@ /obj/machinery/light/bigfloorlamp, /turf/simulated/floor/outdoors/grass/virgo3c, /area/groundbase/level3/escapepad) +"Gk" = ( +/obj/structure/railing{ + dir = 4 + }, +/obj/machinery/light, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Gn" = ( /obj/effect/floor_decal/industrial/warning{ dir = 1 @@ -1942,6 +2095,9 @@ "GW" = ( /turf/simulated/floor/outdoors/sidewalk/virgo3c, /area/groundbase/level3/escapepad) +"Ha" = ( +/turf/simulated/wall, +/area/groundbase/civilian/kitchen) "Hm" = ( /turf/simulated/floor/reinforced/virgo3c{ edge_blending_priority = -1 @@ -2088,6 +2244,15 @@ "IS" = ( /turf/simulated/open/virgo3c, /area/groundbase/level3/ne/open) +"IW" = ( +/obj/structure/railing{ + dir = 1 + }, +/obj/structure/bed/chair/backed_red{ + dir = 8 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Jf" = ( /obj/machinery/hologram/holopad, /turf/simulated/floor/tiled/virgo3c, @@ -2179,6 +2344,10 @@ "KG" = ( /turf/simulated/floor/tiled, /area/groundbase/exploration/equipment) +"KN" = ( +/obj/structure/table/steel, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "KZ" = ( /obj/structure/cable/yellow{ icon_state = "4-8" @@ -2226,6 +2395,12 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/medical/patient3) +"Ms" = ( +/obj/structure/railing{ + dir = 8 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Mz" = ( /obj/structure/railing{ dir = 4 @@ -2294,6 +2469,12 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/medical/paramedic) +"NR" = ( +/obj/structure/sink/kitchen{ + pixel_y = 16 + }, +/turf/simulated/floor/lino, +/area/groundbase/civilian/kitchen) "NT" = ( /obj/machinery/alarm{ dir = 8 @@ -2408,6 +2589,10 @@ /obj/machinery/atmospherics/pipe/simple/hidden/supply, /turf/simulated/floor/tiled/white, /area/groundbase/medical/uhallway) +"Pq" = ( +/obj/structure/table/steel, +/turf/simulated/floor/lino, +/area/groundbase/civilian/kitchen) "Pr" = ( /obj/machinery/button/remote/airlock{ dir = 8; @@ -2488,6 +2673,15 @@ "Qa" = ( /turf/simulated/floor/outdoors/grass/forest/virgo3c, /area/groundbase/level3/nw) +"Qb" = ( +/obj/structure/railing{ + dir = 4 + }, +/obj/machinery/light{ + dir = 1 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Qe" = ( /obj/structure/table/bench/marble, /obj/structure/cable/yellow{ @@ -2523,6 +2717,13 @@ outdoors = 0 }, /area/groundbase/level3/escapepad) +"Qs" = ( +/obj/structure/railing, +/obj/structure/bed/chair/backed_red{ + dir = 8 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "QC" = ( /obj/machinery/door/airlock/multi_tile/glass/polarized{ id_tag = null; @@ -2582,6 +2783,12 @@ "Rq" = ( /turf/simulated/wall/r_wall, /area/groundbase/dorms/bathroom) +"RH" = ( +/obj/structure/railing{ + dir = 1 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "RM" = ( /obj/machinery/photocopier/faxmachine{ department = "Exploration" @@ -2876,6 +3083,10 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/medical/patient1) +"Wh" = ( +/obj/structure/railing, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "Wm" = ( /obj/structure/closet/secure_closet/pilot, /obj/machinery/light{ @@ -2912,6 +3123,13 @@ "Xv" = ( /turf/simulated/floor/outdoors/grass/forest/virgo3c, /area/groundbase/unexplored/outdoors) +"Xz" = ( +/obj/structure/railing, +/obj/machinery/light{ + dir = 4 + }, +/turf/simulated/floor/bmarble, +/area/groundbase/civilian/kitchen) "XB" = ( /obj/structure/bed/padded, /obj/item/weapon/bedsheet/medical, @@ -2952,6 +3170,11 @@ }, /turf/simulated/floor/tiled/white, /area/groundbase/dorms/bathroom) +"YG" = ( +/turf/simulated/floor/virgo3c{ + edge_blending_priority = -1 + }, +/area/groundbase/civilian/kitchen) "YJ" = ( /obj/structure/cable/yellow{ icon_state = "2-8" @@ -5979,7 +6202,7 @@ QD QD QD QD -Kr +ly Kr pf pf @@ -6121,7 +6344,7 @@ QD QD QD QD -Kr +ly Kr pf pf @@ -6263,7 +6486,7 @@ QD QD QD QD -Kr +ly Kr Kr Kr @@ -6401,6 +6624,7 @@ Kr Kr Kr Kr +Kr QO By By @@ -6410,7 +6634,6 @@ Kr Kr Kr Kr -Kr pf pf pf @@ -6543,6 +6766,7 @@ Kr Kr Kr Kr +Kr QO By By @@ -6552,7 +6776,6 @@ Kr Kr Kr Kr -Kr pf pf pf @@ -6685,8 +6908,9 @@ Kr Kr Kr Kr -QO -By +Kr +QD +BM By ly Kr @@ -6694,7 +6918,6 @@ Kr Kr Kr Kr -Kr pf pf pf @@ -6819,22 +7042,22 @@ nU Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG Kr Kr pf @@ -6961,22 +7184,22 @@ pe Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +Ha +nh +Ms +Ms +Ms +Ms +Ms +Ms +Ms +AZ +AZ +Ms +sa +Ha +YG Kr Kr pf @@ -7103,22 +7326,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +Em +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +rr +YG Kr Kr pf @@ -7245,22 +7468,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +nD +Ct +Ct +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +Ct +Ct +hM +YG Kr Kr pf @@ -7387,22 +7610,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +mS +KN +KN +mT +mT +mT +mT +mT +mT +mT +mT +KN +KN +gV +YG Kr Kr pf @@ -7529,22 +7752,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +IW +ud +ud +mT +mT +pP +aN +aN +pP +mT +mT +ud +ud +Qs +YG Kr Kr pf @@ -7671,22 +7894,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +RH +AZ +AZ +mT +yu +Ha +Pq +Pq +Ha +oI +mT +AZ +AZ +Wh +YG Kr Kr pf @@ -7813,22 +8036,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +RH +AZ +AZ +mT +pa +Pq +FU +FU +Pq +Fr +mT +AZ +AZ +Wh +YG Kr Kr pf @@ -7955,22 +8178,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +RH +AZ +AZ +mT +pa +Pq +FU +FU +Pq +Fr +mT +AZ +AZ +Wh +YG Kr Kr pf @@ -8097,22 +8320,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +RH +AZ +AZ +mT +pa +Pq +FU +FU +Pq +Fr +mT +AZ +AZ +Wh +YG Kr Kr pf @@ -8239,22 +8462,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +RH +AZ +AZ +mT +yu +Ha +NR +FU +Ha +oI +mT +AZ +AZ +Wh +YG Kr Kr pf @@ -8381,22 +8604,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +nD +Ct +Ct +mT +mT +mY +mT +mT +mY +mT +mT +Ct +Ct +hM +YG Kr Kr pf @@ -8523,22 +8746,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +mS +fn +KN +mT +mT +mT +mT +mT +mT +mT +mT +KN +KN +gV +YG Kr Kr pf @@ -8665,22 +8888,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +IW +ud +ud +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +ud +ud +Qs +YG Kr Kr pf @@ -8807,22 +9030,22 @@ BE Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +vJ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +AZ +Xz +YG Kr Kr pf @@ -8949,22 +9172,22 @@ pe Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +Ha +Qb +cD +cD +cD +cD +cD +cD +AZ +AZ +cD +cD +Gk +Ha +YG Kr Kr pf @@ -9091,22 +9314,22 @@ VR Kr Kr Kr -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD -QD +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG +YG Kr Kr pf @@ -14891,9 +15114,9 @@ Lf Lf Lf Lf -Lf -Lf -Lf +IS +IS +IS IS IS IS @@ -15033,9 +15256,9 @@ Lf Lf Lf Lf -Lf -Lf -Lf +IS +IS +IS IS IS IS @@ -15175,9 +15398,9 @@ Lf Lf Lf Lf -Lf -Lf -Lf +IS +IS +IS IS IS IS @@ -15317,9 +15540,9 @@ Lf Lf Lf Lf -Lf -Lf -Lf +IS +IS +IS IS IS IS diff --git a/maps/groundbase/southwilds/southwilds1.dmm b/maps/groundbase/southwilds/southwilds1.dmm index f8fe87f859b..f2c050a458e 100644 --- a/maps/groundbase/southwilds/southwilds1.dmm +++ b/maps/groundbase/southwilds/southwilds1.dmm @@ -10,6 +10,12 @@ edge_blending_priority = -1 }, /area/submap/groundbase/wilderness/south) +"o" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/deep/virgo3c, +/area/submap/groundbase/wilderness/south) "p" = ( /turf/simulated/floor/outdoors/grass/virgo3c, /area/submap/groundbase/wilderness/south) @@ -34,6 +40,12 @@ "H" = ( /turf/simulated/floor/outdoors/grass/virgo3c, /area/submap/groundbase/wilderness/south/unexplored) +"I" = ( +/obj/effect/landmark{ + name = "carpspawn" + }, +/turf/simulated/floor/water/virgo3c, +/area/submap/groundbase/wilderness/south) "K" = ( /obj/effect/map_effect/portal/line/side_b, /turf/simulated/floor/outdoors/grass/forest/virgo3c, @@ -1243,7 +1255,7 @@ d d c c -F +o c c c @@ -4771,7 +4783,7 @@ H d d c -c +I c F F @@ -4917,7 +4929,7 @@ c c F F -F +o c c c @@ -6511,7 +6523,7 @@ d d c c -c +I c c c @@ -7085,7 +7097,7 @@ c c c c -F +o F c c @@ -10217,7 +10229,7 @@ d c c c -F +o c c c @@ -11761,7 +11773,7 @@ d c F F -c +I c d d @@ -12927,7 +12939,7 @@ d d c c -c +I c c c @@ -13156,7 +13168,7 @@ c c c c -c +I c c d @@ -13784,7 +13796,7 @@ d d c c -c +I c c c @@ -13931,7 +13943,7 @@ d c c c -F +o F F c @@ -14023,7 +14035,7 @@ F F F F -F +o F F F @@ -14878,7 +14890,7 @@ c c F F -F +o F c c @@ -15234,7 +15246,7 @@ d d c F -F +o c c c @@ -16689,7 +16701,7 @@ d c c F -F +o c c d @@ -17687,7 +17699,7 @@ H d c c -F +o c c d @@ -18497,7 +18509,7 @@ d c c F -F +o F F F @@ -18643,7 +18655,7 @@ F F F F -F +o F c c diff --git a/maps/tether/tether-02-surface2.dmm b/maps/tether/tether-02-surface2.dmm index a412e998d04..6484c366c30 100644 --- a/maps/tether/tether-02-surface2.dmm +++ b/maps/tether/tether-02-surface2.dmm @@ -11238,6 +11238,8 @@ /obj/effect/floor_decal/corner/red/border{ dir = 8 }, +/obj/item/clothing/shoes/magboots, +/obj/item/clothing/shoes/magboots, /turf/simulated/floor/tiled, /area/tether/surfacebase/security/evastorage) "arK" = ( diff --git a/maps/tether/tether-05-station1.dmm b/maps/tether/tether-05-station1.dmm index faa7cd90d36..095f1c4720b 100644 --- a/maps/tether/tether-05-station1.dmm +++ b/maps/tether/tether-05-station1.dmm @@ -15345,6 +15345,7 @@ /obj/machinery/light{ dir = 1 }, +/obj/item/clothing/shoes/magboots, /turf/simulated/floor/tiled, /area/quartermaster/belterdock/gear) "bPl" = ( @@ -22625,6 +22626,7 @@ /obj/effect/floor_decal/corner/brown/bordercorner2{ dir = 4 }, +/obj/item/clothing/shoes/magboots, /turf/simulated/floor/tiled, /area/quartermaster/belterdock/gear) "iEn" = ( @@ -23204,6 +23206,7 @@ /obj/effect/floor_decal/corner/brown/border{ dir = 5 }, +/obj/item/clothing/shoes/magboots, /turf/simulated/floor/tiled, /area/quartermaster/belterdock/gear) "jnG" = ( diff --git a/maps/tether/tether_turfs.dm b/maps/tether/tether_turfs.dm index b219e15f443..bfaa882bc3b 100644 --- a/maps/tether/tether_turfs.dm +++ b/maps/tether/tether_turfs.dm @@ -276,6 +276,9 @@ VIRGO3B_TURF_CREATE(/turf/simulated/mineral/floor) for(var/obj/effect/step_trigger/teleporter/planetary_fall/virgo3b/F in src) qdel(F) +/turf/space/v3b_midpoint/CanZPass(atom, direction) + return 0 // We're not Space + // Tram transit floor /turf/simulated/floor/tiled/techfloor/grid/transit icon = 'icons/turf/transit_vr.dmi' diff --git a/sound/effects/weather/indoorrain_end.ogg b/sound/effects/weather/indoorrain_end.ogg index c82039d95b7..2b83d6b6a44 100644 Binary files a/sound/effects/weather/indoorrain_end.ogg and b/sound/effects/weather/indoorrain_end.ogg differ diff --git a/sound/effects/weather/indoorrain_mid.ogg b/sound/effects/weather/indoorrain_mid.ogg index 60ab5f16240..088aec86ef8 100644 Binary files a/sound/effects/weather/indoorrain_mid.ogg and b/sound/effects/weather/indoorrain_mid.ogg differ diff --git a/sound/effects/weather/indoorrain_start.ogg b/sound/effects/weather/indoorrain_start.ogg index 23165836a1f..ca43575d5f5 100644 Binary files a/sound/effects/weather/indoorrain_start.ogg and b/sound/effects/weather/indoorrain_start.ogg differ diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 334a03e9c25..31d1b345d47 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -670,7 +670,7 @@ rules: # react/sort-prop-types: error ## Enforce the state initialization style to be either in a constructor or ## with a class property - react/state-in-constructor: error + # react/state-in-constructor: error ## Enforces where React component static properties should be positioned. # react/static-property-placement: error ## Enforce style prop value being an object diff --git a/tgui/.gitignore b/tgui/.gitignore index 56cb38b290a..bcdbd1c91a4 100644 --- a/tgui/.gitignore +++ b/tgui/.gitignore @@ -14,6 +14,7 @@ package-lock.json ## Build artifacts /public/.tmp/**/* +/public/*.map ## Previously ignored locations that are kept to avoid confusing git ## while transitioning to a new project structure. diff --git a/tgui/babel.config.js b/tgui/babel.config.js index e2f32480663..d8ddb75721d 100644 --- a/tgui/babel.config.js +++ b/tgui/babel.config.js @@ -5,7 +5,7 @@ */ const createBabelConfig = options => { - const { mode, presets = [], plugins = [] } = options; + const { presets = [], plugins = [], removeConsole } = options; return { presets: [ [require.resolve('@babel/preset-typescript'), { @@ -20,17 +20,17 @@ const createBabelConfig = options => { targets: [], }], ...presets, - ], + ].filter(Boolean), plugins: [ [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true, }], require.resolve('@babel/plugin-transform-jscript'), require.resolve('babel-plugin-inferno'), - require.resolve('babel-plugin-transform-remove-console'), + removeConsole && require.resolve('babel-plugin-transform-remove-console'), require.resolve('common/string.babel-plugin.cjs'), ...plugins, - ], + ].filter(Boolean), }; }; diff --git a/tgui/bin/tgui b/tgui/bin/tgui index dc1d05578ba..256f0e579e1 100644 --- a/tgui/bin/tgui +++ b/tgui/bin/tgui @@ -92,7 +92,7 @@ task-clean() { rm -f .yarn/build-state.yml rm -f .yarn/install-state.gz rm -f .yarn/install-target - rm -f .pnp.js + rm -f .pnp.* ## NPM artifacts rm -rf **/node_modules rm -f **/package-lock.json diff --git a/tgui/bin/tgui-bench.bat b/tgui/bin/tgui-bench.bat new file mode 100644 index 00000000000..da22a7b2ae9 --- /dev/null +++ b/tgui/bin/tgui-bench.bat @@ -0,0 +1,9 @@ +@echo off +rem Copyright (c) 2020 Aleksej Komarov +rem SPDX-License-Identifier: MIT +call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\tgui_.ps1" --bench %* +rem Pause if launched in a separate shell unless initiated from powershell +echo %PSModulePath% | findstr %USERPROFILE% >NUL +if %errorlevel% equ 0 exit 0 +echo %cmdcmdline% | find /i "/c" +if %errorlevel% equ 0 pause diff --git a/tgui/bin/tgui_.ps1 b/tgui/bin/tgui_.ps1 index 54585c6252f..a1909fe70ef 100644 --- a/tgui/bin/tgui_.ps1 +++ b/tgui/bin/tgui_.ps1 @@ -50,6 +50,10 @@ function task-dev-server { yarn node --experimental-modules "packages/tgui-dev-server/index.js" @Args } +function task-bench { + yarn tgui:bench @Args +} + ## Run a linter through all packages function task-lint { yarn run tsc @@ -75,7 +79,7 @@ function task-clean { Remove-Quiet -Force ".yarn\build-state.yml" Remove-Quiet -Force ".yarn\install-state.gz" Remove-Quiet -Force ".yarn\install-target" - Remove-Quiet -Force ".pnp.js" + Remove-Quiet -Force ".pnp.*" ## NPM artifacts Get-ChildItem -Path "." -Include "node_modules" -Recurse -File:$false | Remove-Item -Recurse -Force Remove-Quiet -Force "package-lock.json" @@ -132,6 +136,13 @@ if ($Args.Length -gt 0) { task-webpack --mode=production --analyze exit 0 } + + if ($Args[0] -eq "--bench") { + $Rest = $Args | Select-Object -Skip 1 + task-install + task-bench --wait-on-error + exit 0 + } } ## Make a production webpack build diff --git a/tgui/global.d.ts b/tgui/global.d.ts index aa8495a2498..0f69e371539 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -4,184 +4,180 @@ * @license MIT */ -declare global { - // Webpack asset modules. - // Should match extensions used in webpack config. - declare module '*.png' { - const content: string; - export default content; - } - - declare module '*.jpg' { - const content: string; - export default content; - } - - declare module '*.svg' { - const content: string; - export default content; - } - - type TguiMessage = { - type: string; - payload?: any; - [key: string]: any; - }; - - type ByondType = { - /** - * ID of the Byond window this script is running on. - * Can be used as a parameter to winget/winset. - */ - windowId: string; - - /** - * True if javascript is running in BYOND. - */ - IS_BYOND: boolean; - - /** - * Version of Trident engine of Internet Explorer. Null if N/A. - */ - TRIDENT: number | null; - - /** - * True if browser is IE8 or lower. - */ - IS_LTE_IE8: boolean; - - /** - * True if browser is IE9 or lower. - */ - IS_LTE_IE9: boolean; - - /** - * True if browser is IE10 or lower. - */ - IS_LTE_IE10: boolean; - - /** - * True if browser is IE11 or lower. - */ - IS_LTE_IE11: boolean; - - /** - * Makes a BYOND call. - * - * If path is empty, this will trigger a Topic call. - * You can reference a specific object by setting the "src" parameter. - * - * See: https://secure.byond.com/docs/ref/skinparams.html - */ - call(path: string, params: object): void; - - /** - * Makes an asynchronous BYOND call. Returns a promise. - */ - callAsync(path: string, params: object): Promise; - - /** - * Makes a Topic call. - * - * You can reference a specific object by setting the "src" parameter. - */ - topic(params: object): void; - - /** - * Runs a command or a verb. - */ - command(command: string): void; - - /** - * Retrieves all properties of the BYOND skin element. - * - * Returns a promise with a key-value object containing all properties. - */ - winget(id: string | null): Promise; - - /** - * Retrieves all properties of the BYOND skin element. - * - * Returns a promise with a key-value object containing all properties. - */ - winget(id: string | null, propName: '*'): Promise; - - /** - * Retrieves an exactly one property of the BYOND skin element, - * as defined in `propName`. - * - * Returns a promise with the value of that property. - */ - winget(id: string | null, propName: string): Promise; - - /** - * Retrieves multiple properties of the BYOND skin element, - * as defined in the `propNames` array. - * - * Returns a promise with a key-value object containing listed properties. - */ - winget(id: string | null, propNames: string[]): Promise; - - /** - * Assigns properties to BYOND skin elements in bulk. - */ - winset(props: object): void; - - /** - * Assigns properties to the BYOND skin element. - */ - winset(id: string | null, props: object): void; - - /** - * Sets a property on the BYOND skin element to a certain value. - */ - winset(id: string | null, propName: string, propValue: any): void; - - /** - * Parses BYOND JSON. - * - * Uses a special encoding to preserve `Infinity` and `NaN`. - */ - parseJson(text: string): any; - - /** - * Sends a message to `/datum/tgui_window` which hosts this window instance. - */ - sendMessage(type: string, payload?: any): void; - sendMessage(message: TguiMessage): void; - - /** - * Subscribe to incoming messages that were sent from `/datum/tgui_window`. - */ - subscribe(listener: (type: string, payload: any) => void): void; - - /** - * Subscribe to incoming messages *of some specific type* - * that were sent from `/datum/tgui_window`. - */ - subscribeTo(type: string, listener: (payload: any) => void): void; - - /** - * Loads a stylesheet into the document. - */ - loadCss(url: string): void; - - /** - * Loads a script into the document. - */ - loadJs(url: string): void; - }; - - /** - * Object that provides access to Byond Skin API and is available in - * any tgui application. - */ - const Byond: ByondType; - - interface Window { - Byond: ByondType; - } - +// Webpack asset modules. +// Should match extensions used in webpack config. +declare module '*.png' { + const content: string; + export default content; } -export {}; +declare module '*.jpg' { + const content: string; + export default content; +} + +declare module '*.svg' { + const content: string; + export default content; +} + +type TguiMessage = { + type: string; + payload?: any; + [key: string]: any; +}; + + +type ByondType = { + /** + * ID of the Byond window this script is running on. + * Can be used as a parameter to winget/winset. + */ + windowId: string; + + /** + * True if javascript is running in BYOND. + */ + IS_BYOND: boolean; + + /** + * Version of Trident engine of Internet Explorer. Null if N/A. + */ + TRIDENT: number | null; + + /** + * True if browser is IE8 or lower. + */ + IS_LTE_IE8: boolean; + + /** + * True if browser is IE9 or lower. + */ + IS_LTE_IE9: boolean; + + /** + * True if browser is IE10 or lower. + */ + IS_LTE_IE10: boolean; + + /** + * True if browser is IE11 or lower. + */ + IS_LTE_IE11: boolean; + + /** + * Makes a BYOND call. + * + * If path is empty, this will trigger a Topic call. + * You can reference a specific object by setting the "src" parameter. + * + * See: https://secure.byond.com/docs/ref/skinparams.html + */ + call(path: string, params: object): void; + + /** + * Makes an asynchronous BYOND call. Returns a promise. + */ + callAsync(path: string, params: object): Promise; + + /** + * Makes a Topic call. + * + * You can reference a specific object by setting the "src" parameter. + */ + topic(params: object): void; + + /** + * Runs a command or a verb. + */ + command(command: string): void; + + /** + * Retrieves all properties of the BYOND skin element. + * + * Returns a promise with a key-value object containing all properties. + */ + winget(id: string | null): Promise; + + /** + * Retrieves all properties of the BYOND skin element. + * + * Returns a promise with a key-value object containing all properties. + */ + winget(id: string | null, propName: '*'): Promise; + + /** + * Retrieves an exactly one property of the BYOND skin element, + * as defined in `propName`. + * + * Returns a promise with the value of that property. + */ + winget(id: string | null, propName: string): Promise; + + /** + * Retrieves multiple properties of the BYOND skin element, + * as defined in the `propNames` array. + * + * Returns a promise with a key-value object containing listed properties. + */ + winget(id: string | null, propNames: string[]): Promise; + + /** + * Assigns properties to BYOND skin elements in bulk. + */ + winset(props: object): void; + + /** + * Assigns properties to the BYOND skin element. + */ + winset(id: string | null, props: object): void; + + /** + * Sets a property on the BYOND skin element to a certain value. + */ + winset(id: string | null, propName: string, propValue: any): void; + + /** + * Parses BYOND JSON. + * + * Uses a special encoding to preserve `Infinity` and `NaN`. + */ + parseJson(text: string): any; + + /** + * Sends a message to `/datum/tgui_window` which hosts this window instance. + */ + sendMessage(type: string, payload?: any): void; + sendMessage(message: TguiMessage): void; + + /** + * Subscribe to incoming messages that were sent from `/datum/tgui_window`. + */ + subscribe(listener: (type: string, payload: any) => void): void; + + /** + * Subscribe to incoming messages *of some specific type* + * that were sent from `/datum/tgui_window`. + */ + subscribeTo(type: string, listener: (payload: any) => void): void; + + /** + * Loads a stylesheet into the document. + */ + loadCss(url: string): void; + + /** + * Loads a script into the document. + */ + loadJs(url: string): void; +}; + +/** + * Object that provides access to Byond Skin API and is available in + * any tgui application. + */ +const Byond: ByondType; + +interface Window { + Byond: ByondType; +} diff --git a/tgui/jest.config.js b/tgui/jest.config.js index 5802332817f..e654f0089b8 100644 --- a/tgui/jest.config.js +++ b/tgui/jest.config.js @@ -4,6 +4,9 @@ module.exports = { '/packages/**/__tests__/*.{js,ts,tsx}', '/packages/**/*.{spec,test}.{js,ts,tsx}', ], + testPathIgnorePatterns: [ + '/packages/tgui-bench', + ], testEnvironment: 'jsdom', testRunner: require.resolve('jest-circus/runner'), transform: { diff --git a/tgui/package.json b/tgui/package.json index cdaea681073..7fc6d67cd29 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -6,6 +6,18 @@ "workspaces": [ "packages/*" ], + "scripts": { + "tgui:build": "webpack", + "tgui:analyze": "webpack --analyze", + "tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js", + "tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx", + "tgui:sonar": "eslint packages --ext .js,.cjs,.ts,.tsx -c .eslintrc-harder.yml", + "tgui:tsc": "tsc", + "tgui:test": "jest --watch", + "tgui:test-simple": "CI=true jest --color", + "tgui:test-ci": "CI=true jest --color --collect-coverage", + "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js" + }, "dependencies": { "@babel/core": "^7.18.0", "@babel/eslint-parser": "^7.17.0", @@ -16,6 +28,8 @@ "@types/jest": "^27.5.1", "@types/jsdom": "^16.2.14", "@types/node": "^17.0.35", + "@types/webpack": "^5.28.0", + "@types/webpack-env": "^1.17.0", "@typescript-eslint/parser": "^5.25.0", "babel-jest": "^28.1.0", "babel-loader": "^8.2.5", diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 7abe49ff238..f0a8bfeba31 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -4,64 +4,6 @@ * @license MIT */ -/** - * Converts a given collection to an array. - * - * - Arrays are returned unmodified; - * - If object was provided, keys will be discarded; - * - Everything else will result in an empty array. - * - * @returns {any[]} - */ -export const toArray = collection => { - if (Array.isArray(collection)) { - return collection; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(collection[i]); - } - } - return result; - } - return []; -}; - -/** - * Converts a given object to an array, and appends a key to every - * object inside of that array. - * - * Example input (object): - * ``` - * { - * 'Foo': { info: 'Hello world!' }, - * 'Bar': { info: 'Hello world!' }, - * } - * ``` - * - * Example output (array): - * ``` - * [ - * { key: 'Foo', info: 'Hello world!' }, - * { key: 'Bar', info: 'Hello world!' }, - * ] - * ``` - * - * @template T - * @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array - * @param {string} keyProp Property, to which key will be assigned - * @returns {T[]} Array of keyed objects - */ -export const toKeyedArray = (obj, keyProp = 'key') => { - return map((item, key) => ({ - [keyProp]: key, - ...item, - }))(obj); -}; - /** * Iterates over elements of collection, returning an array of all elements * iteratee returns truthy for. The predicate is invoked with three @@ -72,21 +14,40 @@ export const toKeyedArray = (obj, keyProp = 'key') => { * * @returns {any[]} */ -export const filter = iterateeFn => collection => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - const item = collection[i]; - if (iterateeFn(item, i, collection)) { - result.push(item); +export const filter = (iterateeFn: ( + input: T, + index: number, + collection: T[], +) => boolean) => + (collection: T[]): T[] => { + if (collection === null || collection === undefined) { + return collection; } - } - return result; - } - throw new Error(`filter() can't iterate on type ${typeof collection}`); + if (Array.isArray(collection)) { + const result: T[] = []; + for (let i = 0; i < collection.length; i++) { + const item = collection[i]; + if (iterateeFn(item, i, collection)) { + result.push(item); + } + } + return result; + } + throw new Error(`filter() can't iterate on type ${typeof collection}`); + }; + +type MapFunction = { + (iterateeFn: ( + value: T, + index: number, + collection: T[], + ) => U): (collection: T[]) => U[]; + + (iterateeFn: ( + value: T, + index: K, + collection: Record, + ) => U): (collection: Record) => U[]; }; /** @@ -96,32 +57,25 @@ export const filter = iterateeFn => collection => { * * If collection is 'null' or 'undefined', it will be returned "as is" * without emitting any errors (which can be useful in some cases). - * - * @returns {any[]} */ -export const map = iterateeFn => collection => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - result.push(iterateeFn(collection[i], i, collection)); +export const map: MapFunction = (iterateeFn) => + (collection: T[]): U[] => { + if (collection === null || collection === undefined) { + return collection; } - return result; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(iterateeFn(collection[i], i, collection)); - } + + if (Array.isArray(collection)) { + return collection.map(iterateeFn); } - return result; - } - throw new Error(`map() can't iterate on type ${typeof collection}`); -}; + + if (typeof collection === 'object') { + return Object.entries(collection).map(([key, value]) => { + return iterateeFn(value, key, collection); + }); + } + + throw new Error(`map() can't iterate on type ${typeof collection}`); + }; const COMPARATOR = (objA, objB) => { const criteriaA = objA.criteria; @@ -148,28 +102,35 @@ const COMPARATOR = (objA, objB) => { * * @returns {any[]} */ -export const sortBy = (...iterateeFns) => array => { - if (!Array.isArray(array)) { - return array; - } - let length = array.length; - // Iterate over the array to collect criteria to sort it by - let mappedArray = []; - for (let i = 0; i < length; i++) { - const value = array[i]; - mappedArray.push({ - criteria: iterateeFns.map(fn => fn(value)), - value, - }); - } - // Sort criteria using the base comparator - mappedArray.sort(COMPARATOR); - // Unwrap values - while (length--) { - mappedArray[length] = mappedArray[length].value; - } - return mappedArray; -}; +export const sortBy = ( + ...iterateeFns: ((input: T) => unknown)[] +) => (array: T[]): T[] => { + if (!Array.isArray(array)) { + return array; + } + let length = array.length; + // Iterate over the array to collect criteria to sort it by + let mappedArray: { + criteria: unknown[], + value: T, + }[] = []; + for (let i = 0; i < length; i++) { + const value = array[i]; + mappedArray.push({ + criteria: iterateeFns.map(fn => fn(value)), + value, + }); + } + // Sort criteria using the base comparator + mappedArray.sort(COMPARATOR); + + // Unwrap values + const values: T[] = []; + while (length--) { + values[length] = mappedArray[length].value; + } + return values; + }; export const sort = sortBy(); @@ -212,40 +173,38 @@ export const reduce = (reducerFn, initialValue) => array => { * is determined by the order they occur in the array. The iteratee is * invoked with one argument: value. */ -/* eslint-disable indent */ export const uniqBy = ( iterateeFn?: (value: T) => unknown -) => (array: T[]) => { - const { length } = array; - const result = []; - const seen = iterateeFn ? [] : result; - let index = -1; - outer: - while (++index < length) { - let value: T | 0 = array[index]; - const computed = iterateeFn ? iterateeFn(value) : value; - value = value !== 0 ? value : 0; - if (computed === computed) { - let seenIndex = seen.length; - while (seenIndex--) { - if (seen[seenIndex] === computed) { - continue outer; +) => (array: T[]): T[] => { + const { length } = array; + const result: T[] = []; + const seen: unknown[] = iterateeFn ? [] : result; + let index = -1; + outer: + while (++index < length) { + let value: T | 0 = array[index]; + const computed = iterateeFn ? iterateeFn(value) : value; + if (computed === computed) { + let seenIndex = seen.length; + while (seenIndex--) { + if (seen[seenIndex] === computed) { + continue outer; + } } + if (iterateeFn) { + seen.push(computed); + } + result.push(value); } - if (iterateeFn) { - seen.push(computed); + else if (!seen.includes(computed)) { + if (seen !== result) { + seen.push(computed); + } + result.push(value); } - result.push(value); } - else if (!seen.includes(computed)) { - if (seen !== result) { - seen.push(computed); - } - result.push(value); - } - } - return result; -}; + return result; + }; /* eslint-enable indent */ export const uniq = uniqBy(); @@ -261,17 +220,19 @@ type Zip = { */ export const zip = (...arrays: T): Zip => { if (arrays.length === 0) { - return; + return []; } const numArrays = arrays.length; const numValues = arrays[0].length; - const result = []; + const result: Zip = []; for (let valueIndex = 0; valueIndex < numValues; valueIndex++) { - const entry = []; + const entry: unknown[] = []; for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) { entry.push(arrays[arrayIndex][valueIndex]); } - result.push(entry); + + // I tried everything to remove this any, and have no idea how to do it. + result.push(entry as any); } return result; }; @@ -280,9 +241,8 @@ export const zip = (...arrays: T): Zip => { * This method is like "zip" except that it accepts iteratee to * specify how grouped values should be combined. The iteratee is * invoked with the elements of each group. - * - * @returns {any[]} */ -export const zipWith = iterateeFn => (...arrays) => { - return map(values => iterateeFn(...values))(zip(...arrays)); -}; +export const zipWith = (iterateeFn: (...values: T[]) => U) => + (...arrays: T[][]): U[] => { + return map((values: T[]) => iterateeFn(...values))(zip(...arrays)); + }; diff --git a/tgui/packages/common/types.ts b/tgui/packages/common/types.ts new file mode 100644 index 00000000000..a92ac122d9f --- /dev/null +++ b/tgui/packages/common/types.ts @@ -0,0 +1,5 @@ +/** + * Returns the arguments of a function F as an array. + */ +export type ArgumentsOf + = F extends (...args: infer A) => unknown ? A : never; diff --git a/tgui/packages/tgfont/dist/tgfont.css b/tgui/packages/tgfont/dist/tgfont.css index 6295dcce8fa..ad732b8fc06 100644 --- a/tgui/packages/tgfont/dist/tgfont.css +++ b/tgui/packages/tgfont/dist/tgfont.css @@ -1,7 +1,7 @@ @font-face { font-family: "tgfont"; - src: url("./tgfont.woff2?8fcc44d209cc0a286e2fedc5edea15e7") format("woff2"), -url("./tgfont.eot?8fcc44d209cc0a286e2fedc5edea15e7#iefix") format("embedded-opentype"); + src: url("./tgfont.woff2?45c3c7acc69dd413375d77898d24e41e") format("woff2"), +url("./tgfont.eot?45c3c7acc69dd413375d77898d24e41e#iefix") format("embedded-opentype"); } i[class^="tg-"]:before, i[class*=" tg-"]:before { @@ -21,21 +21,30 @@ i[class^="tg-"]:before, i[class*=" tg-"]:before { .tg-air-tank:before { content: "\f102"; } -.tg-image-minus:before { +.tg-bad-touch:before { content: "\f103"; } -.tg-image-plus:before { +.tg-image-minus:before { content: "\f104"; } -.tg-nanotrasen-logo:before { +.tg-image-plus:before { content: "\f105"; } -.tg-sound-minus:before { +.tg-nanotrasen-logo:before { content: "\f106"; } -.tg-sound-plus:before { +.tg-non-binary:before { content: "\f107"; } -.tg-syndicate-logo:before { +.tg-prosthetic-leg:before { content: "\f108"; } +.tg-sound-minus:before { + content: "\f109"; +} +.tg-sound-plus:before { + content: "\f10a"; +} +.tg-syndicate-logo:before { + content: "\f10b"; +} diff --git a/tgui/packages/tgfont/dist/tgfont.eot b/tgui/packages/tgfont/dist/tgfont.eot index a1ffb10df47..ba307e06729 100644 Binary files a/tgui/packages/tgfont/dist/tgfont.eot and b/tgui/packages/tgfont/dist/tgfont.eot differ diff --git a/tgui/packages/tgfont/dist/tgfont.woff2 b/tgui/packages/tgfont/dist/tgfont.woff2 index 4e2dae731a6..3f54573899c 100644 Binary files a/tgui/packages/tgfont/dist/tgfont.woff2 and b/tgui/packages/tgfont/dist/tgfont.woff2 differ diff --git a/tgui/packages/tgfont/icons/ATTRIBUTIONS.md b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md new file mode 100644 index 00000000000..2f218388d36 --- /dev/null +++ b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md @@ -0,0 +1,6 @@ +bad-touch.svg contains: +- hug by Phạm Thanh Lộc from the Noun Project +- Fight by Rudez Studio from the Noun Project + +prosthetic-leg.svg contains: +- prosthetic leg by Gan Khoon Lay from the Noun Project diff --git a/tgui/packages/tgfont/icons/bad-touch.svg b/tgui/packages/tgfont/icons/bad-touch.svg new file mode 100644 index 00000000000..a2566c1670a --- /dev/null +++ b/tgui/packages/tgfont/icons/bad-touch.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/non-binary.svg b/tgui/packages/tgfont/icons/non-binary.svg new file mode 100644 index 00000000000..9aaec674bbb --- /dev/null +++ b/tgui/packages/tgfont/icons/non-binary.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/prosthetic-leg.svg b/tgui/packages/tgfont/icons/prosthetic-leg.svg new file mode 100644 index 00000000000..c1f6ceee3fc --- /dev/null +++ b/tgui/packages/tgfont/icons/prosthetic-leg.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/tgui/packages/tgfont/package.json b/tgui/packages/tgfont/package.json index 90c38f82db9..5e5a3f060c8 100644 --- a/tgui/packages/tgfont/package.json +++ b/tgui/packages/tgfont/package.json @@ -2,10 +2,10 @@ "private": true, "name": "tgfont", "version": "1.0.0", - "dependencies": { - "fantasticon": "^1.2.3" - }, "scripts": { "build": "node mkdist.cjs && fantasticon --config config.cjs" + }, + "dependencies": { + "fantasticon": "^1.2.3" } } diff --git a/tgui/packages/tgui-bench/entrypoint.tsx b/tgui/packages/tgui-bench/entrypoint.tsx new file mode 100644 index 00000000000..d41d667894a --- /dev/null +++ b/tgui/packages/tgui-bench/entrypoint.tsx @@ -0,0 +1,73 @@ +/** + * @file + * @copyright 2021 Aleksej Komarov + * @license MIT + */ + +import { setupGlobalEvents } from 'tgui/events'; +import 'tgui/styles/main.scss'; +import Benchmark from './lib/benchmark'; + +const sendMessage = (obj: any) => { + const req = new XMLHttpRequest(); + req.open('POST', `/message`, false); + req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + // req.timeout = 250; + req.send(JSON.stringify(obj)); +}; + +const setupApp = async () => { + // Delay setup + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupApp); + return; + } + + setupGlobalEvents({ + ignoreWindowFocus: true, + }); + + const requireTest = require.context('./tests', false, /\.test\./); + + for (const file of requireTest.keys()) { + sendMessage({ type: 'suite-start', file }); + try { + const tests = requireTest(file); + await new Promise((resolve) => { + const suite = new Benchmark.Suite(file, { + onCycle(e) { + sendMessage({ + type: 'suite-cycle', + message: String(e.target), + }); + }, + onComplete() { + // This message is somewhat useless, but leaving it here in case + // someone has an idea how to show more useful data. + // sendMessage({ + // type: 'suite-complete', + // message: 'Fastest is ' + this.filter('fastest').map('name'), + // }); + resolve(); + }, + onError(e) { + sendMessage({ type: 'error', e }); + resolve(); + }, + }); + for (const [name, fn] of Object.entries(tests)) { + if (typeof fn === 'function') { + suite.add(name, fn); + } + } + suite.run(); + }); + } + catch (error) { + sendMessage({ type: 'error', error }); + } + } + sendMessage({ type: 'finished' }); +}; + +setupApp(); diff --git a/tgui/packages/tgui-bench/index.js b/tgui/packages/tgui-bench/index.js new file mode 100644 index 00000000000..e29ad9a5c20 --- /dev/null +++ b/tgui/packages/tgui-bench/index.js @@ -0,0 +1,93 @@ +/** + * @file + * @copyright 2021 Aleksej Komarov + * @license MIT + */ + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { fastify } = require('fastify'); + +process.chdir(__dirname); + +const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time)); + +const IE_TIMEOUT_SECONDS = 60; + +const setup = async () => { + const server = fastify(); + + let hasResponded = false; + + let assets = ''; + assets += `\n`; + + const publicDir = path.resolve(__dirname, '../../public'); + const page = fs.readFileSync(path.join(publicDir, 'tgui.html'), 'utf-8') + .replace('\n', assets); + + server.register(require('@fastify/static'), { + root: publicDir, + }); + + server.get('/', async (req, res) => { + return res.type('text/html').send(page); + }); + + server.post('/message', async (req, res) => { + if (!hasResponded) { + process.stdout.write('\n'); + hasResponded = true; + } + const { type, ...rest } = req.body; + if (type === 'suite-start') { + console.log(`=> Test '${rest.file}'`); + return res.send(); + } + if (type === 'suite-cycle') { + console.log(rest.message); + return res.send(); + } + if (type === 'suite-complete') { + console.log(rest.message); + return res.send(); + } + if (type === 'finished') { + await res.send(); + process.exit(0); + } + // Unhandled message + console.log(req.body); + return res.send(); + }); + + try { + await server.listen(3002, '0.0.0.0'); + } + catch (err) { + console.error(err); + process.exit(1); + } + + if (process.platform === 'win32') { + exec(`start "" "iexplore" "http://127.0.0.1:3002"`); + } + + console.log('Waiting for Internet Explorer to respond.'); + for (let i = 0; i < IE_TIMEOUT_SECONDS; i++) { + await sleep(1000); + if (hasResponded) { + return; + } + process.stdout.write('.'); + } + process.stdout.write('\n'); + console.error('Did not receive a response, exiting.'); + process.exit(1); +}; + +setup(); diff --git a/tgui/packages/tgui-bench/lib/benchmark.d.ts b/tgui/packages/tgui-bench/lib/benchmark.d.ts new file mode 100644 index 00000000000..7f3310005f7 --- /dev/null +++ b/tgui/packages/tgui-bench/lib/benchmark.d.ts @@ -0,0 +1,219 @@ +/* eslint-disable */ +// Type definitions for Benchmark v2.1.4 +// Project: https://benchmarkjs.com +// Definitions by: Asana +// Charlie Fish +// Blair Zajac +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare class Benchmark { + static filter(arr: T[], callback: (value: T) => any, thisArg?: any): T[]; + static filter(arr: T[], filter: string, thisArg?: any): T[]; + static formatNumber(num: number): string; + static join(obj: Object, separator1?: string, separator2?: string): string; + static invoke( + benches: Benchmark[], + name: string | Object, + ...args: any[] + ): any[]; + static runInContext(context: Object): Function; + + static each(obj: Object | any[], callback: Function, thisArg?: any): void; + static forEach(arr: T[], callback: (value: T) => any, thisArg?: any): void; + static forOwn(obj: Object, callback: Function, thisArg?: any): void; + static has(obj: Object, path: any[] | string): boolean; + static indexOf(arr: T[], value: T, fromIndex?: number): number; + static map(arr: T[], callback: (value: T) => K, thisArg?: any): K[]; + static reduce( + arr: T[], + callback: (accumulator: K, value: T) => K, + thisArg?: any + ): K; + + static options: Benchmark.Options; + static platform: Benchmark.Platform; + static support: Benchmark.Support; + static version: string; + + constructor(fn: Function | string, options?: Benchmark.Options); + constructor(name: string, fn: Function | string, options?: Benchmark.Options); + constructor(name: string, options?: Benchmark.Options); + constructor(options: Benchmark.Options); + + id: number; + name: string; + count: number; + cycles: number; + hz: number; + compiled: Function | string; + error: Error; + fn: Function | string; + aborted: boolean; + running: boolean; + setup: Function | string; + teardown: Function | string; + + stats: Benchmark.Stats; + times: Benchmark.Times; + + abort(): Benchmark; + clone(options: Benchmark.Options): Benchmark; + compare(benchmark: Benchmark): number; + emit(type: string | Object): any; + listeners(type: string): Function[]; + off(type?: string, listener?: Function): Benchmark; + off(types: string[]): Benchmark; + on(type?: string, listener?: Function): Benchmark; + on(types: string[]): Benchmark; + reset(): Benchmark; + run(options?: Benchmark.Options): Benchmark; + toString(): string; +} + +declare namespace Benchmark { + export interface Options { + async?: boolean | undefined; + defer?: boolean | undefined; + delay?: number | undefined; + id?: string | undefined; + initCount?: number | undefined; + maxTime?: number | undefined; + minSamples?: number | undefined; + minTime?: number | undefined; + name?: string | undefined; + onAbort?: Function | undefined; + onComplete?: Function | undefined; + onCycle?: Function | undefined; + onError?: Function | undefined; + onReset?: Function | undefined; + onStart?: Function | undefined; + setup?: Function | string | undefined; + teardown?: Function | string | undefined; + fn?: Function | string | undefined; + queued?: boolean | undefined; + } + + export interface Platform { + description: string; + layout: string; + product: string; + name: string; + manufacturer: string; + os: string; + prerelease: string; + version: string; + toString(): string; + } + + export interface Support { + browser: boolean; + timeout: boolean; + decompilation: boolean; + } + + export interface Stats { + moe: number; + rme: number; + sem: number; + deviation: number; + mean: number; + sample: any[]; + variance: number; + } + + export interface Times { + cycle: number; + elapsed: number; + period: number; + timeStamp: number; + } + + export class Deferred { + constructor(clone: Benchmark); + + benchmark: Benchmark; + cycles: number; + elapsed: number; + timeStamp: number; + + resolve(): void; + } + + export interface Target { + options: Options; + async?: boolean | undefined; + defer?: boolean | undefined; + delay?: number | undefined; + initCount?: number | undefined; + maxTime?: number | undefined; + minSamples?: number | undefined; + minTime?: number | undefined; + name?: string | undefined; + fn?: Function | undefined; + id: number; + stats?: Stats | undefined; + times?: Times | undefined; + running: boolean; + count?: number | undefined; + compiled?: Function | undefined; + cycles?: number | undefined; + hz?: number | undefined; + } + + export class Event { + constructor(type: string | Object); + + aborted: boolean; + cancelled: boolean; + currentTarget: Object; + result: any; + target: Target; + timeStamp: number; + type: string; + } + + export class Suite { + static options: { name: string }; + + constructor(name?: string, options?: Options); + + length: number; + aborted: boolean; + running: boolean; + + abort(): Suite; + add(name: string, fn: Function | string, options?: Options): Suite; + add(fn: Function | string, options?: Options): Suite; + add(name: string, options?: Options): Suite; + add(options: Options): Suite; + clone(options: Options): Suite; + emit(type: string | Object): any; + filter(callback: Function | string): Suite; + join(separator?: string): string; + listeners(type: string): Function[]; + off(type?: string, callback?: Function): Suite; + off(types: string[]): Suite; + on(type?: string, callback?: Function): Suite; + on(types: string[]): Suite; + push(benchmark: Benchmark): number; + reset(): Suite; + run(options?: Options): Suite; + reverse(): any[]; + sort(compareFn: (a: any, b: any) => number): any[]; + splice(start: number, deleteCount?: number): any[]; + unshift(benchmark: Benchmark): number; + + each(callback: Function): Suite; + forEach(callback: Function): Suite; + indexOf(value: any): number; + map(callback: Function | string): any[]; + reduce(callback: Function, accumulator: T): T; + + pop(): Function; + shift(): Benchmark; + slice(start: number, end: number): any[]; + slice(start: number, deleteCount: number, ...values: any[]): any[]; + } +} + +export = Benchmark; diff --git a/tgui/packages/tgui-bench/lib/benchmark.js b/tgui/packages/tgui-bench/lib/benchmark.js new file mode 100644 index 00000000000..1e76cec7087 --- /dev/null +++ b/tgui/packages/tgui-bench/lib/benchmark.js @@ -0,0 +1,2759 @@ +/* eslint-disable */ +/*! + * Benchmark.js + * Copyright 2010-2016 Mathias Bynens + * Based on JSLitmus.js, copyright Robert Kieffer + * Modified by John-David Dalton + * Manually stripped from useless junk by /tg/station13 maintainers. + * Available under MIT license + */ +module.exports = (function() { + 'use strict'; + + /** Used as a safe reference for `undefined` in pre ES5 environments. */ + var undefined; + + /** Used to determine if values are of the language type Object. */ + var objectTypes = { + 'function': true, + 'object': true + }; + + /** Used as a reference to the global object. */ + var root = (objectTypes[typeof window] && window) || this; + + /** Detect free variable `define`. */ + var freeDefine = false; + + /** Used to assign each benchmark an incremented id. */ + var counter = 0; + + /** Used to detect primitive types. */ + var rePrimitive = /^(?:boolean|number|string|undefined)$/; + + /** Used to make every compiled test unique. */ + var uidCounter = 0; + + /** Used to assign default `context` object properties. */ + var contextProps = [ + 'Array', 'Date', 'Function', 'Math', 'Object', 'RegExp', 'String', '_', + 'clearTimeout', 'chrome', 'chromium', 'document', 'navigator', 'phantom', + 'platform', 'process', 'runtime', 'setTimeout' + ]; + + /** Used to avoid hz of Infinity. */ + var divisors = { + '1': 4096, + '2': 512, + '3': 64, + '4': 8, + '5': 0 + }; + + /** + * T-Distribution two-tailed critical values for 95% confidence. + * For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm. + */ + var tTable = { + '1': 12.706, '2': 4.303, '3': 3.182, '4': 2.776, '5': 2.571, '6': 2.447, + '7': 2.365, '8': 2.306, '9': 2.262, '10': 2.228, '11': 2.201, '12': 2.179, + '13': 2.16, '14': 2.145, '15': 2.131, '16': 2.12, '17': 2.11, '18': 2.101, + '19': 2.093, '20': 2.086, '21': 2.08, '22': 2.074, '23': 2.069, '24': 2.064, + '25': 2.06, '26': 2.056, '27': 2.052, '28': 2.048, '29': 2.045, '30': 2.042, + 'infinity': 1.96 + }; + + /** + * Critical Mann-Whitney U-values for 95% confidence. + * For more info see http://www.saburchill.com/IBbiology/stats/003.html. + */ + var uTable = { + '5': [0, 1, 2], + '6': [1, 2, 3, 5], + '7': [1, 3, 5, 6, 8], + '8': [2, 4, 6, 8, 10, 13], + '9': [2, 4, 7, 10, 12, 15, 17], + '10': [3, 5, 8, 11, 14, 17, 20, 23], + '11': [3, 6, 9, 13, 16, 19, 23, 26, 30], + '12': [4, 7, 11, 14, 18, 22, 26, 29, 33, 37], + '13': [4, 8, 12, 16, 20, 24, 28, 33, 37, 41, 45], + '14': [5, 9, 13, 17, 22, 26, 31, 36, 40, 45, 50, 55], + '15': [5, 10, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59, 64], + '16': [6, 11, 15, 21, 26, 31, 37, 42, 47, 53, 59, 64, 70, 75], + '17': [6, 11, 17, 22, 28, 34, 39, 45, 51, 57, 63, 67, 75, 81, 87], + '18': [7, 12, 18, 24, 30, 36, 42, 48, 55, 61, 67, 74, 80, 86, 93, 99], + '19': [7, 13, 19, 25, 32, 38, 45, 52, 58, 65, 72, 78, 85, 92, 99, 106, 113], + '20': [8, 14, 20, 27, 34, 41, 48, 55, 62, 69, 76, 83, 90, 98, 105, 112, 119, 127], + '21': [8, 15, 22, 29, 36, 43, 50, 58, 65, 73, 80, 88, 96, 103, 111, 119, 126, 134, 142], + '22': [9, 16, 23, 30, 38, 45, 53, 61, 69, 77, 85, 93, 101, 109, 117, 125, 133, 141, 150, 158], + '23': [9, 17, 24, 32, 40, 48, 56, 64, 73, 81, 89, 98, 106, 115, 123, 132, 140, 149, 157, 166, 175], + '24': [10, 17, 25, 33, 42, 50, 59, 67, 76, 85, 94, 102, 111, 120, 129, 138, 147, 156, 165, 174, 183, 192], + '25': [10, 18, 27, 35, 44, 53, 62, 71, 80, 89, 98, 107, 117, 126, 135, 145, 154, 163, 173, 182, 192, 201, 211], + '26': [11, 19, 28, 37, 46, 55, 64, 74, 83, 93, 102, 112, 122, 132, 141, 151, 161, 171, 181, 191, 200, 210, 220, 230], + '27': [11, 20, 29, 38, 48, 57, 67, 77, 87, 97, 107, 118, 125, 138, 147, 158, 168, 178, 188, 199, 209, 219, 230, 240, 250], + '28': [12, 21, 30, 40, 50, 60, 70, 80, 90, 101, 111, 122, 132, 143, 154, 164, 175, 186, 196, 207, 218, 228, 239, 250, 261, 272], + '29': [13, 22, 32, 42, 52, 62, 73, 83, 94, 105, 116, 127, 138, 149, 160, 171, 182, 193, 204, 215, 226, 238, 249, 260, 271, 282, 294], + '30': [13, 23, 33, 43, 54, 65, 76, 87, 98, 109, 120, 131, 143, 154, 166, 177, 189, 200, 212, 223, 235, 247, 258, 270, 282, 293, 305, 317] + }; + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new `Benchmark` function using the given `context` object. + * + * @static + * @memberOf Benchmark + * @param {Object} [context=root] The context object. + * @returns {Function} Returns a new `Benchmark` function. + */ + function runInContext(context) { + // Exit early if unable to acquire lodash. + var _ = context && context._ || require('lodash') || root._; + if (!_) { + Benchmark.runInContext = runInContext; + return Benchmark; + } + // Avoid issues with some ES3 environments that attempt to use values, named + // after built-in constructors like `Object`, for the creation of literals. + // ES5 clears this up by stating that literals must use built-in constructors. + // See http://es5.github.io/#x11.1.5. + context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root; + + /** Native constructor references. */ + var Array = context.Array, + Date = context.Date, + Function = context.Function, + Math = context.Math, + Object = context.Object, + RegExp = context.RegExp, + String = context.String; + + /** Used for `Array` and `Object` method references. */ + var arrayRef = [], + objectProto = Object.prototype; + + /** Native method shortcuts. */ + var abs = Math.abs, + clearTimeout = context.clearTimeout, + floor = Math.floor, + log = Math.log, + max = Math.max, + min = Math.min, + pow = Math.pow, + push = arrayRef.push, + setTimeout = context.setTimeout, + shift = arrayRef.shift, + slice = arrayRef.slice, + sqrt = Math.sqrt, + toString = objectProto.toString, + unshift = arrayRef.unshift; + + /** Detect DOM document object. */ + var doc = isHostType(context, 'document') && context.document; + + /** Used to access Node.js's high resolution timer. */ + var processObject = isHostType(context, 'process') && context.process; + + /** Used to prevent a `removeChild` memory leak in IE < 9. */ + var trash = doc && doc.createElement('div'); + + /** Used to integrity check compiled tests. */ + var uid = 'uid' + _.now(); + + /** Used to avoid infinite recursion when methods call each other. */ + var calledBy = {}; + + /** + * An object used to flag environments/features. + * + * @static + * @memberOf Benchmark + * @type Object + */ + var support = {}; + + (function() { + + /** + * Detect if running in a browser environment. + * + * @memberOf Benchmark.support + * @type boolean + */ + support.browser = doc && isHostType(context, 'navigator') && !isHostType(context, 'phantom'); + + /** + * Detect if the Timers API exists. + * + * @memberOf Benchmark.support + * @type boolean + */ + support.timeout = isHostType(context, 'setTimeout') && isHostType(context, 'clearTimeout'); + + /** + * Detect if function decompilation is support. + * + * @name decompilation + * @memberOf Benchmark.support + * @type boolean + */ + try { + // Safari 2.x removes commas in object literals from `Function#toString` results. + // See http://webk.it/11609 for more details. + // Firefox 3.6 and Opera 9.25 strip grouping parentheses from `Function#toString` results. + // See http://bugzil.la/559438 for more details. + support.decompilation = Function( + ('return (' + (function(x) { return { 'x': '' + (1 + x) + '', 'y': 0 }; }) + ')') + // Avoid issues with code added by Istanbul. + .replace(/__cov__[^;]+;/g, '') + )()(0).x === '1'; + } catch(e) { + support.decompilation = false; + } + }()); + + /** + * Timer object used by `clock()` and `Deferred#resolve`. + * + * @private + * @type Object + */ + var timer = { + + /** + * The timer namespace object or constructor. + * + * @private + * @memberOf timer + * @type {Function|Object} + */ + 'ns': Date, + + /** + * Starts the deferred timer. + * + * @private + * @memberOf timer + * @param {Object} deferred The deferred instance. + */ + 'start': null, // Lazy defined in `clock()`. + + /** + * Stops the deferred timer. + * + * @private + * @memberOf timer + * @param {Object} deferred The deferred instance. + */ + 'stop': null // Lazy defined in `clock()`. + }; + + /*------------------------------------------------------------------------*/ + + /** + * The Benchmark constructor. + * + * Note: The Benchmark constructor exposes a handful of lodash methods to + * make working with arrays, collections, and objects easier. The lodash + * methods are: + * [`each/forEach`](https://lodash.com/docs#forEach), [`forOwn`](https://lodash.com/docs#forOwn), + * [`has`](https://lodash.com/docs#has), [`indexOf`](https://lodash.com/docs#indexOf), + * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) + * + * @constructor + * @param {string} name A name to identify the benchmark. + * @param {Function|string} fn The test to benchmark. + * @param {Object} [options={}] Options object. + * @example + * + * // basic usage (the `new` operator is optional) + * var bench = new Benchmark(fn); + * + * // or using a name first + * var bench = new Benchmark('foo', fn); + * + * // or with options + * var bench = new Benchmark('foo', fn, { + * + * // displayed by `Benchmark#toString` if `name` is not available + * 'id': 'xyz', + * + * // called when the benchmark starts running + * 'onStart': onStart, + * + * // called after each run cycle + * 'onCycle': onCycle, + * + * // called when aborted + * 'onAbort': onAbort, + * + * // called when a test errors + * 'onError': onError, + * + * // called when reset + * 'onReset': onReset, + * + * // called when the benchmark completes running + * 'onComplete': onComplete, + * + * // compiled/called before the test loop + * 'setup': setup, + * + * // compiled/called after the test loop + * 'teardown': teardown + * }); + * + * // or name and options + * var bench = new Benchmark('foo', { + * + * // a flag to indicate the benchmark is deferred + * 'defer': true, + * + * // benchmark test function + * 'fn': function(deferred) { + * // call `Deferred#resolve` when the deferred test is finished + * deferred.resolve(); + * } + * }); + * + * // or options only + * var bench = new Benchmark({ + * + * // benchmark name + * 'name': 'foo', + * + * // benchmark test as a string + * 'fn': '[1,2,3,4].sort()' + * }); + * + * // a test's `this` binding is set to the benchmark instance + * var bench = new Benchmark('foo', function() { + * 'My name is '.concat(this.name); // "My name is foo" + * }); + */ + function Benchmark(name, fn, options) { + var bench = this; + + // Allow instance creation without the `new` operator. + if (!(bench instanceof Benchmark)) { + return new Benchmark(name, fn, options); + } + // Juggle arguments. + if (_.isPlainObject(name)) { + // 1 argument (options). + options = name; + } + else if (_.isFunction(name)) { + // 2 arguments (fn, options). + options = fn; + fn = name; + } + else if (_.isPlainObject(fn)) { + // 2 arguments (name, options). + options = fn; + fn = null; + bench.name = name; + } + else { + // 3 arguments (name, fn [, options]). + bench.name = name; + } + setOptions(bench, options); + + bench.id || (bench.id = ++counter); + bench.fn == null && (bench.fn = fn); + + bench.stats = cloneDeep(bench.stats); + bench.times = cloneDeep(bench.times); + } + + /** + * The Deferred constructor. + * + * @constructor + * @memberOf Benchmark + * @param {Object} clone The cloned benchmark instance. + */ + function Deferred(clone) { + var deferred = this; + if (!(deferred instanceof Deferred)) { + return new Deferred(clone); + } + deferred.benchmark = clone; + clock(deferred); + } + + /** + * The Event constructor. + * + * @constructor + * @memberOf Benchmark + * @param {Object|string} type The event type. + */ + function Event(type) { + var event = this; + if (type instanceof Event) { + return type; + } + return (event instanceof Event) + ? _.assign(event, { 'timeStamp': _.now() }, typeof type == 'string' ? { 'type': type } : type) + : new Event(type); + } + + /** + * The Suite constructor. + * + * Note: Each Suite instance has a handful of wrapped lodash methods to + * make working with Suites easier. The wrapped lodash methods are: + * [`each/forEach`](https://lodash.com/docs#forEach), [`indexOf`](https://lodash.com/docs#indexOf), + * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) + * + * @constructor + * @memberOf Benchmark + * @param {string} name A name to identify the suite. + * @param {Object} [options={}] Options object. + * @example + * + * // basic usage (the `new` operator is optional) + * var suite = new Benchmark.Suite; + * + * // or using a name first + * var suite = new Benchmark.Suite('foo'); + * + * // or with options + * var suite = new Benchmark.Suite('foo', { + * + * // called when the suite starts running + * 'onStart': onStart, + * + * // called between running benchmarks + * 'onCycle': onCycle, + * + * // called when aborted + * 'onAbort': onAbort, + * + * // called when a test errors + * 'onError': onError, + * + * // called when reset + * 'onReset': onReset, + * + * // called when the suite completes running + * 'onComplete': onComplete + * }); + */ + function Suite(name, options) { + var suite = this; + + // Allow instance creation without the `new` operator. + if (!(suite instanceof Suite)) { + return new Suite(name, options); + } + // Juggle arguments. + if (_.isPlainObject(name)) { + // 1 argument (options). + options = name; + } else { + // 2 arguments (name [, options]). + suite.name = name; + } + setOptions(suite, options); + } + + /*------------------------------------------------------------------------*/ + + /** + * A specialized version of `_.cloneDeep` which only clones arrays and plain + * objects assigning all other values by reference. + * + * @private + * @param {*} value The value to clone. + * @returns {*} The cloned value. + */ + var cloneDeep = _.partial(_.cloneDeepWith, _, function(value) { + // Only clone primitives, arrays, and plain objects. + return (_.isObject(value) && !_.isArray(value) && !_.isPlainObject(value)) + ? value + : undefined; + }); + + /** + * Creates a function from the given arguments string and body. + * + * @private + * @param {string} args The comma separated function arguments. + * @param {string} body The function body. + * @returns {Function} The new function. + */ + function createFunction() { + // Lazy define. + createFunction = function(args, body) { + var result, + anchor = freeDefine ? freeDefine.amd : Benchmark, + prop = uid + 'createFunction'; + + runScript((freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '=function(' + args + '){' + body + '}'); + result = anchor[prop]; + delete anchor[prop]; + return result; + }; + // Fix JaegerMonkey bug. + // For more information see http://bugzil.la/639720. + createFunction = support.browser && (createFunction('', 'return"' + uid + '"') || _.noop)() == uid ? createFunction : Function; + return createFunction.apply(null, arguments); + } + + /** + * Delay the execution of a function based on the benchmark's `delay` property. + * + * @private + * @param {Object} bench The benchmark instance. + * @param {Object} fn The function to execute. + */ + function delay(bench, fn) { + bench._timerId = _.delay(fn, bench.delay * 1e3); + } + + /** + * Destroys the given element. + * + * @private + * @param {Element} element The element to destroy. + */ + function destroyElement(element) { + trash.appendChild(element); + trash.innerHTML = ''; + } + + /** + * Gets the name of the first argument from a function's source. + * + * @private + * @param {Function} fn The function. + * @returns {string} The argument name. + */ + function getFirstArgument(fn) { + return (!_.has(fn, 'toString') && + (/^[\s(]*function[^(]*\(([^\s,)]+)/.exec(fn) || 0)[1]) || ''; + } + + /** + * Computes the arithmetic mean of a sample. + * + * @private + * @param {Array} sample The sample. + * @returns {number} The mean. + */ + function getMean(sample) { + return (_.reduce(sample, function(sum, x) { + return sum + x; + }) / sample.length) || 0; + } + + /** + * Gets the source code of a function. + * + * @private + * @param {Function} fn The function. + * @returns {string} The function's source code. + */ + function getSource(fn) { + var result = ''; + if (isStringable(fn)) { + result = String(fn); + } else if (support.decompilation) { + // Escape the `{` for Firefox 1. + result = _.result(/^[^{]+\{([\s\S]*)\}\s*$/.exec(fn), 1); + } + // Trim string. + result = (result || '').replace(/^\s+|\s+$/g, ''); + + // Detect strings containing only the "use strict" directive. + return /^(?:\/\*+[\w\W]*?\*\/|\/\/.*?[\n\r\u2028\u2029]|\s)*(["'])use strict\1;?$/.test(result) + ? '' + : result; + } + + /** + * Checks if an object is of the specified class. + * + * @private + * @param {*} value The value to check. + * @param {string} name The name of the class. + * @returns {boolean} Returns `true` if the value is of the specified class, else `false`. + */ + function isClassOf(value, name) { + return value != null && toString.call(value) == '[object ' + name + ']'; + } + + /** + * Host objects can return type values that are different from their actual + * data type. The objects we are concerned with usually return non-primitive + * types of "object", "function", or "unknown". + * + * @private + * @param {*} object The owner of the property. + * @param {string} property The property to check. + * @returns {boolean} Returns `true` if the property value is a non-primitive, else `false`. + */ + function isHostType(object, property) { + if (object == null) { + return false; + } + var type = typeof object[property]; + return !rePrimitive.test(type) && (type != 'object' || !!object[property]); + } + + /** + * Checks if a value can be safely coerced to a string. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the value can be coerced, else `false`. + */ + function isStringable(value) { + return _.isString(value) || (_.has(value, 'toString') && _.isFunction(value.toString)); + } + + /** + * Runs a snippet of JavaScript via script injection. + * + * @private + * @param {string} code The code to run. + */ + function runScript(code) { + var anchor = freeDefine ? define.amd : Benchmark, + script = doc.createElement('script'), + sibling = doc.getElementsByTagName('script')[0], + parent = sibling.parentNode, + prop = uid + 'runScript', + prefix = '(' + (freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '||function(){})();'; + + // Firefox 2.0.0.2 cannot use script injection as intended because it executes + // asynchronously, but that's OK because script injection is only used to avoid + // the previously commented JaegerMonkey bug. + try { + // Remove the inserted script *before* running the code to avoid differences + // in the expected script element count/order of the document. + script.appendChild(doc.createTextNode(prefix + code)); + anchor[prop] = function() { destroyElement(script); }; + } catch(e) { + parent = parent.cloneNode(false); + sibling = null; + script.text = code; + } + parent.insertBefore(script, sibling); + delete anchor[prop]; + } + + /** + * A helper function for setting options/event handlers. + * + * @private + * @param {Object} object The benchmark or suite instance. + * @param {Object} [options={}] Options object. + */ + function setOptions(object, options) { + options = object.options = _.assign({}, cloneDeep(object.constructor.options), cloneDeep(options)); + + _.forOwn(options, function(value, key) { + if (value != null) { + // Add event listeners. + if (/^on[A-Z]/.test(key)) { + _.each(key.split(' '), function(key) { + object.on(key.slice(2).toLowerCase(), value); + }); + } else if (!_.has(object, key)) { + object[key] = cloneDeep(value); + } + } + }); + } + + /*------------------------------------------------------------------------*/ + + /** + * Handles cycling/completing the deferred benchmark. + * + * @memberOf Benchmark.Deferred + */ + function resolve() { + var deferred = this, + clone = deferred.benchmark, + bench = clone._original; + + if (bench.aborted) { + // cycle() -> clone cycle/complete event -> compute()'s invoked bench.run() cycle/complete. + deferred.teardown(); + clone.running = false; + cycle(deferred); + } + else if (++deferred.cycles < clone.count) { + clone.compiled.call(deferred, context, timer); + } + else { + timer.stop(deferred); + deferred.teardown(); + delay(clone, function() { cycle(deferred); }); + } + } + + /*------------------------------------------------------------------------*/ + + /** + * A generic `Array#filter` like method. + * + * @static + * @memberOf Benchmark + * @param {Array} array The array to iterate over. + * @param {Function|string} callback The function/alias called per iteration. + * @returns {Array} A new array of values that passed callback filter. + * @example + * + * // get odd numbers + * Benchmark.filter([1, 2, 3, 4, 5], function(n) { + * return n % 2; + * }); // -> [1, 3, 5]; + * + * // get fastest benchmarks + * Benchmark.filter(benches, 'fastest'); + * + * // get slowest benchmarks + * Benchmark.filter(benches, 'slowest'); + * + * // get benchmarks that completed without erroring + * Benchmark.filter(benches, 'successful'); + */ + function filter(array, callback) { + if (callback === 'successful') { + // Callback to exclude those that are errored, unrun, or have hz of Infinity. + callback = function(bench) { + return bench.cycles && _.isFinite(bench.hz) && !bench.error; + }; + } + else if (callback === 'fastest' || callback === 'slowest') { + // Get successful, sort by period + margin of error, and filter fastest/slowest. + var result = filter(array, 'successful').sort(function(a, b) { + a = a.stats; b = b.stats; + return (a.mean + a.moe > b.mean + b.moe ? 1 : -1) * (callback === 'fastest' ? 1 : -1); + }); + + return _.filter(result, function(bench) { + return result[0].compare(bench) == 0; + }); + } + return _.filter(array, callback); + } + + /** + * Converts a number to a more readable comma-separated string representation. + * + * @static + * @memberOf Benchmark + * @param {number} number The number to convert. + * @returns {string} The more readable string representation. + */ + function formatNumber(number) { + number = String(number).split('.'); + return number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + + (number[1] ? '.' + number[1] : ''); + } + + /** + * Invokes a method on all items in an array. + * + * @static + * @memberOf Benchmark + * @param {Array} benches Array of benchmarks to iterate over. + * @param {Object|string} name The name of the method to invoke OR options object. + * @param {...*} [args] Arguments to invoke the method with. + * @returns {Array} A new array of values returned from each method invoked. + * @example + * + * // invoke `reset` on all benchmarks + * Benchmark.invoke(benches, 'reset'); + * + * // invoke `emit` with arguments + * Benchmark.invoke(benches, 'emit', 'complete', listener); + * + * // invoke `run(true)`, treat benchmarks as a queue, and register invoke callbacks + * Benchmark.invoke(benches, { + * + * // invoke the `run` method + * 'name': 'run', + * + * // pass a single argument + * 'args': true, + * + * // treat as queue, removing benchmarks from front of `benches` until empty + * 'queued': true, + * + * // called before any benchmarks have been invoked. + * 'onStart': onStart, + * + * // called between invoking benchmarks + * 'onCycle': onCycle, + * + * // called after all benchmarks have been invoked. + * 'onComplete': onComplete + * }); + */ + function invoke(benches, name) { + var args, + bench, + queued, + index = -1, + eventProps = { 'currentTarget': benches }, + options = { 'onStart': _.noop, 'onCycle': _.noop, 'onComplete': _.noop }, + result = _.toArray(benches); + + /** + * Invokes the method of the current object and if synchronous, fetches the next. + */ + function execute() { + var listeners, + async = isAsync(bench); + + if (async) { + // Use `getNext` as the first listener. + bench.on('complete', getNext); + listeners = bench.events.complete; + listeners.splice(0, 0, listeners.pop()); + } + // Execute method. + result[index] = _.isFunction(bench && bench[name]) ? bench[name].apply(bench, args) : undefined; + // If synchronous return `true` until finished. + return !async && getNext(); + } + + /** + * Fetches the next bench or executes `onComplete` callback. + */ + function getNext(event) { + var cycleEvent, + last = bench, + async = isAsync(last); + + if (async) { + last.off('complete', getNext); + last.emit('complete'); + } + // Emit "cycle" event. + eventProps.type = 'cycle'; + eventProps.target = last; + cycleEvent = Event(eventProps); + options.onCycle.call(benches, cycleEvent); + + // Choose next benchmark if not exiting early. + if (!cycleEvent.aborted && raiseIndex() !== false) { + bench = queued ? benches[0] : result[index]; + if (isAsync(bench)) { + delay(bench, execute); + } + else if (async) { + // Resume execution if previously asynchronous but now synchronous. + while (execute()) {} + } + else { + // Continue synchronous execution. + return true; + } + } else { + // Emit "complete" event. + eventProps.type = 'complete'; + options.onComplete.call(benches, Event(eventProps)); + } + // When used as a listener `event.aborted = true` will cancel the rest of + // the "complete" listeners because they were already called above and when + // used as part of `getNext` the `return false` will exit the execution while-loop. + if (event) { + event.aborted = true; + } else { + return false; + } + } + + /** + * Checks if invoking `Benchmark#run` with asynchronous cycles. + */ + function isAsync(object) { + // Avoid using `instanceof` here because of IE memory leak issues with host objects. + var async = args[0] && args[0].async; + return name == 'run' && (object instanceof Benchmark) && + ((async == null ? object.options.async : async) && support.timeout || object.defer); + } + + /** + * Raises `index` to the next defined index or returns `false`. + */ + function raiseIndex() { + index++; + + // If queued remove the previous bench. + if (queued && index > 0) { + shift.call(benches); + } + // If we reached the last index then return `false`. + return (queued ? benches.length : index < result.length) + ? index + : (index = false); + } + // Juggle arguments. + if (_.isString(name)) { + // 2 arguments (array, name). + args = slice.call(arguments, 2); + } else { + // 2 arguments (array, options). + options = _.assign(options, name); + name = options.name; + args = _.isArray(args = 'args' in options ? options.args : []) ? args : [args]; + queued = options.queued; + } + // Start iterating over the array. + if (raiseIndex() !== false) { + // Emit "start" event. + bench = result[index]; + eventProps.type = 'start'; + eventProps.target = bench; + options.onStart.call(benches, Event(eventProps)); + + // End early if the suite was aborted in an "onStart" listener. + if (name == 'run' && (benches instanceof Suite) && benches.aborted) { + // Emit "cycle" event. + eventProps.type = 'cycle'; + options.onCycle.call(benches, Event(eventProps)); + // Emit "complete" event. + eventProps.type = 'complete'; + options.onComplete.call(benches, Event(eventProps)); + } + // Start method execution. + else { + if (isAsync(bench)) { + delay(bench, execute); + } else { + while (execute()) {} + } + } + } + return result; + } + + /** + * Creates a string of joined array values or object key-value pairs. + * + * @static + * @memberOf Benchmark + * @param {Array|Object} object The object to operate on. + * @param {string} [separator1=','] The separator used between key-value pairs. + * @param {string} [separator2=': '] The separator used between keys and values. + * @returns {string} The joined result. + */ + function join(object, separator1, separator2) { + var result = [], + length = (object = Object(object)).length, + arrayLike = length === length >>> 0; + + separator2 || (separator2 = ': '); + _.each(object, function(value, key) { + result.push(arrayLike ? value : key + separator2 + value); + }); + return result.join(separator1 || ','); + } + + /*------------------------------------------------------------------------*/ + + /** + * Aborts all benchmarks in the suite. + * + * @name abort + * @memberOf Benchmark.Suite + * @returns {Object} The suite instance. + */ + function abortSuite() { + var event, + suite = this, + resetting = calledBy.resetSuite; + + if (suite.running) { + event = Event('abort'); + suite.emit(event); + if (!event.cancelled || resetting) { + // Avoid infinite recursion. + calledBy.abortSuite = true; + suite.reset(); + delete calledBy.abortSuite; + + if (!resetting) { + suite.aborted = true; + invoke(suite, 'abort'); + } + } + } + return suite; + } + + /** + * Adds a test to the benchmark suite. + * + * @memberOf Benchmark.Suite + * @param {string} name A name to identify the benchmark. + * @param {Function|string} fn The test to benchmark. + * @param {Object} [options={}] Options object. + * @returns {Object} The suite instance. + * @example + * + * // basic usage + * suite.add(fn); + * + * // or using a name first + * suite.add('foo', fn); + * + * // or with options + * suite.add('foo', fn, { + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + * + * // or name and options + * suite.add('foo', { + * 'fn': fn, + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + * + * // or options only + * suite.add({ + * 'name': 'foo', + * 'fn': fn, + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + */ + function add(name, fn, options) { + var suite = this, + bench = new Benchmark(name, fn, options), + event = Event({ 'type': 'add', 'target': bench }); + + if (suite.emit(event), !event.cancelled) { + suite.push(bench); + } + return suite; + } + + /** + * Creates a new suite with cloned benchmarks. + * + * @name clone + * @memberOf Benchmark.Suite + * @param {Object} options Options object to overwrite cloned options. + * @returns {Object} The new suite instance. + */ + function cloneSuite(options) { + var suite = this, + result = new suite.constructor(_.assign({}, suite.options, options)); + + // Copy own properties. + _.forOwn(suite, function(value, key) { + if (!_.has(result, key)) { + result[key] = value && _.isFunction(value.clone) + ? value.clone() + : cloneDeep(value); + } + }); + return result; + } + + /** + * An `Array#filter` like method. + * + * @name filter + * @memberOf Benchmark.Suite + * @param {Function|string} callback The function/alias called per iteration. + * @returns {Object} A new suite of benchmarks that passed callback filter. + */ + function filterSuite(callback) { + var suite = this, + result = new suite.constructor(suite.options); + + result.push.apply(result, filter(suite, callback)); + return result; + } + + /** + * Resets all benchmarks in the suite. + * + * @name reset + * @memberOf Benchmark.Suite + * @returns {Object} The suite instance. + */ + function resetSuite() { + var event, + suite = this, + aborting = calledBy.abortSuite; + + if (suite.running && !aborting) { + // No worries, `resetSuite()` is called within `abortSuite()`. + calledBy.resetSuite = true; + suite.abort(); + delete calledBy.resetSuite; + } + // Reset if the state has changed. + else if ((suite.aborted || suite.running) && + (suite.emit(event = Event('reset')), !event.cancelled)) { + suite.aborted = suite.running = false; + if (!aborting) { + invoke(suite, 'reset'); + } + } + return suite; + } + + /** + * Runs the suite. + * + * @name run + * @memberOf Benchmark.Suite + * @param {Object} [options={}] Options object. + * @returns {Object} The suite instance. + * @example + * + * // basic usage + * suite.run(); + * + * // or with options + * suite.run({ 'async': true, 'queued': true }); + */ + function runSuite(options) { + var suite = this; + + suite.reset(); + suite.running = true; + options || (options = {}); + + invoke(suite, { + 'name': 'run', + 'args': options, + 'queued': options.queued, + 'onStart': function(event) { + suite.emit(event); + }, + 'onCycle': function(event) { + var bench = event.target; + if (bench.error) { + suite.emit({ 'type': 'error', 'target': bench }); + } + suite.emit(event); + event.aborted = suite.aborted; + }, + 'onComplete': function(event) { + suite.running = false; + suite.emit(event); + } + }); + return suite; + } + + /*------------------------------------------------------------------------*/ + + /** + * Executes all registered listeners of the specified event type. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {Object|string} type The event type or object. + * @param {...*} [args] Arguments to invoke the listener with. + * @returns {*} Returns the return value of the last listener executed. + */ + function emit(type) { + var listeners, + object = this, + event = Event(type), + events = object.events, + args = (arguments[0] = event, arguments); + + event.currentTarget || (event.currentTarget = object); + event.target || (event.target = object); + delete event.result; + + if (events && (listeners = _.has(events, event.type) && events[event.type])) { + _.each(listeners.slice(), function(listener) { + if ((event.result = listener.apply(object, args)) === false) { + event.cancelled = true; + } + return !event.aborted; + }); + } + return event.result; + } + + /** + * Returns an array of event listeners for a given type that can be manipulated + * to add or remove listeners. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} type The event type. + * @returns {Array} The listeners array. + */ + function listeners(type) { + var object = this, + events = object.events || (object.events = {}); + + return _.has(events, type) ? events[type] : (events[type] = []); + } + + /** + * Unregisters a listener for the specified event type(s), + * or unregisters all listeners for the specified event type(s), + * or unregisters all listeners for all event types. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} [type] The event type. + * @param {Function} [listener] The function to unregister. + * @returns {Object} The current instance. + * @example + * + * // unregister a listener for an event type + * bench.off('cycle', listener); + * + * // unregister a listener for multiple event types + * bench.off('start cycle', listener); + * + * // unregister all listeners for an event type + * bench.off('cycle'); + * + * // unregister all listeners for multiple event types + * bench.off('start cycle complete'); + * + * // unregister all listeners for all event types + * bench.off(); + */ + function off(type, listener) { + var object = this, + events = object.events; + + if (!events) { + return object; + } + _.each(type ? type.split(' ') : events, function(listeners, type) { + var index; + if (typeof listeners == 'string') { + type = listeners; + listeners = _.has(events, type) && events[type]; + } + if (listeners) { + if (listener) { + index = _.indexOf(listeners, listener); + if (index > -1) { + listeners.splice(index, 1); + } + } else { + listeners.length = 0; + } + } + }); + return object; + } + + /** + * Registers a listener for the specified event type(s). + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} type The event type. + * @param {Function} listener The function to register. + * @returns {Object} The current instance. + * @example + * + * // register a listener for an event type + * bench.on('cycle', listener); + * + * // register a listener for multiple event types + * bench.on('start cycle', listener); + */ + function on(type, listener) { + var object = this, + events = object.events || (object.events = {}); + + _.each(type.split(' '), function(type) { + (_.has(events, type) + ? events[type] + : (events[type] = []) + ).push(listener); + }); + return object; + } + + /*------------------------------------------------------------------------*/ + + /** + * Aborts the benchmark without recording times. + * + * @memberOf Benchmark + * @returns {Object} The benchmark instance. + */ + function abort() { + var event, + bench = this, + resetting = calledBy.reset; + + if (bench.running) { + event = Event('abort'); + bench.emit(event); + if (!event.cancelled || resetting) { + // Avoid infinite recursion. + calledBy.abort = true; + bench.reset(); + delete calledBy.abort; + + if (support.timeout) { + clearTimeout(bench._timerId); + delete bench._timerId; + } + if (!resetting) { + bench.aborted = true; + bench.running = false; + } + } + } + return bench; + } + + /** + * Creates a new benchmark using the same test and options. + * + * @memberOf Benchmark + * @param {Object} options Options object to overwrite cloned options. + * @returns {Object} The new benchmark instance. + * @example + * + * var bizarro = bench.clone({ + * 'name': 'doppelganger' + * }); + */ + function clone(options) { + var bench = this, + result = new bench.constructor(_.assign({}, bench, options)); + + // Correct the `options` object. + result.options = _.assign({}, cloneDeep(bench.options), cloneDeep(options)); + + // Copy own custom properties. + _.forOwn(bench, function(value, key) { + if (!_.has(result, key)) { + result[key] = cloneDeep(value); + } + }); + + return result; + } + + /** + * Determines if a benchmark is faster than another. + * + * @memberOf Benchmark + * @param {Object} other The benchmark to compare. + * @returns {number} Returns `-1` if slower, `1` if faster, and `0` if indeterminate. + */ + function compare(other) { + var bench = this; + + // Exit early if comparing the same benchmark. + if (bench == other) { + return 0; + } + var critical, + zStat, + sample1 = bench.stats.sample, + sample2 = other.stats.sample, + size1 = sample1.length, + size2 = sample2.length, + maxSize = max(size1, size2), + minSize = min(size1, size2), + u1 = getU(sample1, sample2), + u2 = getU(sample2, sample1), + u = min(u1, u2); + + function getScore(xA, sampleB) { + return _.reduce(sampleB, function(total, xB) { + return total + (xB > xA ? 0 : xB < xA ? 1 : 0.5); + }, 0); + } + + function getU(sampleA, sampleB) { + return _.reduce(sampleA, function(total, xA) { + return total + getScore(xA, sampleB); + }, 0); + } + + function getZ(u) { + return (u - ((size1 * size2) / 2)) / sqrt((size1 * size2 * (size1 + size2 + 1)) / 12); + } + // Reject the null hypothesis the two samples come from the + // same population (i.e. have the same median) if... + if (size1 + size2 > 30) { + // ...the z-stat is greater than 1.96 or less than -1.96 + // http://www.statisticslectures.com/topics/mannwhitneyu/ + zStat = getZ(u); + return abs(zStat) > 1.96 ? (u == u1 ? 1 : -1) : 0; + } + // ...the U value is less than or equal the critical U value. + critical = maxSize < 5 || minSize < 3 ? 0 : uTable[maxSize][minSize - 3]; + return u <= critical ? (u == u1 ? 1 : -1) : 0; + } + + /** + * Reset properties and abort if running. + * + * @memberOf Benchmark + * @returns {Object} The benchmark instance. + */ + function reset() { + var bench = this; + if (bench.running && !calledBy.abort) { + // No worries, `reset()` is called within `abort()`. + calledBy.reset = true; + bench.abort(); + delete calledBy.reset; + return bench; + } + var event, + index = 0, + changes = [], + queue = []; + + // A non-recursive solution to check if properties have changed. + // For more information see http://www.jslab.dk/articles/non.recursive.preorder.traversal.part4. + var data = { + 'destination': bench, + 'source': _.assign({}, cloneDeep(bench.constructor.prototype), cloneDeep(bench.options)) + }; + + do { + _.forOwn(data.source, function(value, key) { + var changed, + destination = data.destination, + currValue = destination[key]; + + // Skip pseudo private properties like `_timerId` which could be a + // Java object in environments like RingoJS. + if (key.charAt(0) == '_') { + return; + } + if (value && typeof value == 'object') { + if (_.isArray(value)) { + // Check if an array value has changed to a non-array value. + if (!_.isArray(currValue)) { + changed = currValue = []; + } + // Check if an array has changed its length. + if (currValue.length != value.length) { + changed = currValue = currValue.slice(0, value.length); + currValue.length = value.length; + } + } + // Check if an object has changed to a non-object value. + else if (!currValue || typeof currValue != 'object') { + changed = currValue = {}; + } + // Register a changed object. + if (changed) { + changes.push({ 'destination': destination, 'key': key, 'value': currValue }); + } + queue.push({ 'destination': currValue, 'source': value }); + } + // Register a changed primitive. + else if (value !== currValue && !(value == null || _.isFunction(value))) { + changes.push({ 'destination': destination, 'key': key, 'value': value }); + } + }); + } + while ((data = queue[index++])); + + // If changed emit the `reset` event and if it isn't cancelled reset the benchmark. + if (changes.length && (bench.emit(event = Event('reset')), !event.cancelled)) { + _.each(changes, function(data) { + data.destination[data.key] = data.value; + }); + } + return bench; + } + + /** + * Displays relevant benchmark information when coerced to a string. + * + * @name toString + * @memberOf Benchmark + * @returns {string} A string representation of the benchmark instance. + */ + function toStringBench() { + var bench = this, + error = bench.error, + hz = bench.hz, + id = bench.id, + stats = bench.stats, + size = stats.sample.length, + pm = '\xb1', + result = bench.name || (_.isNaN(id) ? id : ''); + + if (error) { + var errorStr; + if (!_.isObject(error)) { + errorStr = String(error); + } else if (!_.isError(Error)) { + errorStr = join(error); + } else { + // Error#name and Error#message properties are non-enumerable. + errorStr = join(_.assign({ 'name': error.name, 'message': error.message }, error)); + } + result += ': ' + errorStr; + } + else { + result += ' x ' + formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + ' ops/sec ' + pm + + stats.rme.toFixed(2) + '% (' + size + ' run' + (size == 1 ? '' : 's') + ' sampled)'; + } + return result; + } + + /*------------------------------------------------------------------------*/ + + /** + * Clocks the time taken to execute a test per cycle (secs). + * + * @private + * @param {Object} bench The benchmark instance. + * @returns {number} The time taken. + */ + function clock() { + var options = Benchmark.options, + templateData = {}, + timers = [{ 'ns': timer.ns, 'res': max(0.0015, getRes('ms')), 'unit': 'ms' }]; + + // Lazy define for hi-res timers. + clock = function(clone) { + var deferred; + + if (clone instanceof Deferred) { + deferred = clone; + clone = deferred.benchmark; + } + var bench = clone._original, + stringable = isStringable(bench.fn), + count = bench.count = clone.count, + decompilable = stringable || (support.decompilation && (clone.setup !== _.noop || clone.teardown !== _.noop)), + id = bench.id, + name = bench.name || (typeof id == 'number' ? '' : id), + result = 0; + + // Init `minTime` if needed. + clone.minTime = bench.minTime || (bench.minTime = bench.options.minTime = options.minTime); + + // Compile in setup/teardown functions and the test loop. + // Create a new compiled test, instead of using the cached `bench.compiled`, + // to avoid potential engine optimizations enabled over the life of the test. + var funcBody = deferred + ? 'var d#=this,${fnArg}=d#,m#=d#.benchmark._original,f#=m#.fn,su#=m#.setup,td#=m#.teardown;' + + // When `deferred.cycles` is `0` then... + 'if(!d#.cycles){' + + // set `deferred.fn`, + 'd#.fn=function(){var ${fnArg}=d#;if(typeof f#=="function"){try{${fn}\n}catch(e#){f#(d#)}}else{${fn}\n}};' + + // set `deferred.teardown`, + 'd#.teardown=function(){d#.cycles=0;if(typeof td#=="function"){try{${teardown}\n}catch(e#){td#()}}else{${teardown}\n}};' + + // execute the benchmark's `setup`, + 'if(typeof su#=="function"){try{${setup}\n}catch(e#){su#()}}else{${setup}\n};' + + // start timer, + 't#.start(d#);' + + // and then execute `deferred.fn` and return a dummy object. + '}d#.fn();return{uid:"${uid}"}' + + : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count,n#=t#.ns;${setup}\n${begin};' + + 'while(i#--){${fn}\n}${end};${teardown}\nreturn{elapsed:r#,uid:"${uid}"}'; + + var compiled = bench.compiled = clone.compiled = createCompiled(bench, decompilable, deferred, funcBody), + isEmpty = !(templateData.fn || stringable); + + try { + if (isEmpty) { + // Firefox may remove dead code from `Function#toString` results. + // For more information see http://bugzil.la/536085. + throw new Error('The test "' + name + '" is empty. This may be the result of dead code removal.'); + } + else if (!deferred) { + // Pretest to determine if compiled code exits early, usually by a + // rogue `return` statement, by checking for a return object with the uid. + bench.count = 1; + compiled = decompilable && (compiled.call(bench, context, timer) || {}).uid == templateData.uid && compiled; + bench.count = count; + } + } catch(e) { + compiled = null; + clone.error = e || new Error(String(e)); + bench.count = count; + } + // Fallback when a test exits early or errors during pretest. + if (!compiled && !deferred && !isEmpty) { + funcBody = ( + stringable || (decompilable && !clone.error) + ? 'function f#(){${fn}\n}var r#,s#,m#=this,i#=m#.count' + : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count' + ) + + ',n#=t#.ns;${setup}\n${begin};m#.f#=f#;while(i#--){m#.f#()}${end};' + + 'delete m#.f#;${teardown}\nreturn{elapsed:r#}'; + + compiled = createCompiled(bench, decompilable, deferred, funcBody); + + try { + // Pretest one more time to check for errors. + bench.count = 1; + compiled.call(bench, context, timer); + bench.count = count; + delete clone.error; + } + catch(e) { + bench.count = count; + if (!clone.error) { + clone.error = e || new Error(String(e)); + } + } + } + // If no errors run the full test loop. + if (!clone.error) { + compiled = bench.compiled = clone.compiled = createCompiled(bench, decompilable, deferred, funcBody); + result = compiled.call(deferred || bench, context, timer).elapsed; + } + return result; + }; + + /*----------------------------------------------------------------------*/ + + /** + * Creates a compiled function from the given function `body`. + */ + function createCompiled(bench, decompilable, deferred, body) { + var fn = bench.fn, + fnArg = deferred ? getFirstArgument(fn) || 'deferred' : ''; + + templateData.uid = uid + uidCounter++; + + _.assign(templateData, { + 'setup': decompilable ? getSource(bench.setup) : interpolate('m#.setup()'), + 'fn': decompilable ? getSource(fn) : interpolate('m#.fn(' + fnArg + ')'), + 'fnArg': fnArg, + 'teardown': decompilable ? getSource(bench.teardown) : interpolate('m#.teardown()') + }); + + // Use API of chosen timer. + if (timer.unit == 'ns') { + _.assign(templateData, { + 'begin': interpolate('s#=n#()'), + 'end': interpolate('r#=n#(s#);r#=r#[0]+(r#[1]/1e9)') + }); + } + else if (timer.unit == 'us') { + if (timer.ns.stop) { + _.assign(templateData, { + 'begin': interpolate('s#=n#.start()'), + 'end': interpolate('r#=n#.microseconds()/1e6') + }); + } else { + _.assign(templateData, { + 'begin': interpolate('s#=n#()'), + 'end': interpolate('r#=(n#()-s#)/1e6') + }); + } + } + else if (timer.ns.now) { + _.assign(templateData, { + 'begin': interpolate('s#=n#.now()'), + 'end': interpolate('r#=(n#.now()-s#)/1e3') + }); + } + else { + _.assign(templateData, { + 'begin': interpolate('s#=new n#().getTime()'), + 'end': interpolate('r#=(new n#().getTime()-s#)/1e3') + }); + } + // Define `timer` methods. + timer.start = createFunction( + interpolate('o#'), + interpolate('var n#=this.ns,${begin};o#.elapsed=0;o#.timeStamp=s#') + ); + + timer.stop = createFunction( + interpolate('o#'), + interpolate('var n#=this.ns,s#=o#.timeStamp,${end};o#.elapsed=r#') + ); + + // Create compiled test. + return createFunction( + interpolate('window,t#'), + 'var global = window, clearTimeout = global.clearTimeout, setTimeout = global.setTimeout;\n' + + interpolate(body) + ); + } + + /** + * Gets the current timer's minimum resolution (secs). + */ + function getRes(unit) { + var measured, + begin, + count = 30, + divisor = 1e3, + ns = timer.ns, + sample = []; + + // Get average smallest measurable time. + while (count--) { + if (unit == 'us') { + divisor = 1e6; + if (ns.stop) { + ns.start(); + while (!(measured = ns.microseconds())) {} + } else { + begin = ns(); + while (!(measured = ns() - begin)) {} + } + } + else if (unit == 'ns') { + divisor = 1e9; + begin = (begin = ns())[0] + (begin[1] / divisor); + while (!(measured = ((measured = ns())[0] + (measured[1] / divisor)) - begin)) {} + divisor = 1; + } + else if (ns.now) { + begin = ns.now(); + while (!(measured = ns.now() - begin)) {} + } + else { + begin = new ns().getTime(); + while (!(measured = new ns().getTime() - begin)) {} + } + // Check for broken timers. + if (measured > 0) { + sample.push(measured); + } else { + sample.push(Infinity); + break; + } + } + // Convert to seconds. + return getMean(sample) / divisor; + } + + /** + * Interpolates a given template string. + */ + function interpolate(string) { + // Replaces all occurrences of `#` with a unique number and template tokens with content. + return _.template(string.replace(/\#/g, /\d+/.exec(templateData.uid)))(templateData); + } + + /*----------------------------------------------------------------------*/ + + // Detect Chrome's microsecond timer: + // enable benchmarking via the --enable-benchmarking command + // line switch in at least Chrome 7 to use chrome.Interval + try { + if ((timer.ns = new (context.chrome || context.chromium).Interval)) { + timers.push({ 'ns': timer.ns, 'res': getRes('us'), 'unit': 'us' }); + } + } catch(e) {} + + // Detect Node.js's nanosecond resolution timer available in Node.js >= 0.8. + if (processObject && typeof (timer.ns = processObject.hrtime) == 'function') { + timers.push({ 'ns': timer.ns, 'res': getRes('ns'), 'unit': 'ns' }); + } + // Pick timer with highest resolution. + timer = _.minBy(timers, 'res'); + + // Error if there are no working timers. + if (timer.res == Infinity) { + throw new Error('Benchmark.js was unable to find a working timer.'); + } + // Resolve time span required to achieve a percent uncertainty of at most 1%. + // For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html. + options.minTime || (options.minTime = max(timer.res / 2 / 0.01, 0.05)); + return clock.apply(null, arguments); + } + + /*------------------------------------------------------------------------*/ + + /** + * Computes stats on benchmark results. + * + * @private + * @param {Object} bench The benchmark instance. + * @param {Object} options The options object. + */ + function compute(bench, options) { + options || (options = {}); + + var async = options.async, + elapsed = 0, + initCount = bench.initCount, + minSamples = bench.minSamples, + queue = [], + sample = bench.stats.sample; + + /** + * Adds a clone to the queue. + */ + function enqueue() { + queue.push(bench.clone({ + '_original': bench, + 'events': { + 'abort': [update], + 'cycle': [update], + 'error': [update], + 'start': [update] + } + })); + } + + /** + * Updates the clone/original benchmarks to keep their data in sync. + */ + function update(event) { + var clone = this, + type = event.type; + + if (bench.running) { + if (type == 'start') { + // Note: `clone.minTime` prop is inited in `clock()`. + clone.count = bench.initCount; + } + else { + if (type == 'error') { + bench.error = clone.error; + } + if (type == 'abort') { + bench.abort(); + bench.emit('cycle'); + } else { + event.currentTarget = event.target = bench; + bench.emit(event); + } + } + } else if (bench.aborted) { + // Clear abort listeners to avoid triggering bench's abort/cycle again. + clone.events.abort.length = 0; + clone.abort(); + } + } + + /** + * Determines if more clones should be queued or if cycling should stop. + */ + function evaluate(event) { + var critical, + df, + mean, + moe, + rme, + sd, + sem, + variance, + clone = event.target, + done = bench.aborted, + now = _.now(), + size = sample.push(clone.times.period), + maxedOut = size >= minSamples && (elapsed += now - clone.times.timeStamp) / 1e3 > bench.maxTime, + times = bench.times, + varOf = function(sum, x) { return sum + pow(x - mean, 2); }; + + // Exit early for aborted or unclockable tests. + if (done || clone.hz == Infinity) { + maxedOut = !(size = sample.length = queue.length = 0); + } + + if (!done) { + // Compute the sample mean (estimate of the population mean). + mean = getMean(sample); + // Compute the sample variance (estimate of the population variance). + variance = _.reduce(sample, varOf, 0) / (size - 1) || 0; + // Compute the sample standard deviation (estimate of the population standard deviation). + sd = sqrt(variance); + // Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean). + sem = sd / sqrt(size); + // Compute the degrees of freedom. + df = size - 1; + // Compute the critical value. + critical = tTable[Math.round(df) || 1] || tTable.infinity; + // Compute the margin of error. + moe = sem * critical; + // Compute the relative margin of error. + rme = (moe / mean) * 100 || 0; + + _.assign(bench.stats, { + 'deviation': sd, + 'mean': mean, + 'moe': moe, + 'rme': rme, + 'sem': sem, + 'variance': variance + }); + + // Abort the cycle loop when the minimum sample size has been collected + // and the elapsed time exceeds the maximum time allowed per benchmark. + // We don't count cycle delays toward the max time because delays may be + // increased by browsers that clamp timeouts for inactive tabs. For more + // information see https://developer.mozilla.org/en/window.setTimeout#Inactive_tabs. + if (maxedOut) { + // Reset the `initCount` in case the benchmark is rerun. + bench.initCount = initCount; + bench.running = false; + done = true; + times.elapsed = (now - times.timeStamp) / 1e3; + } + if (bench.hz != Infinity) { + bench.hz = 1 / mean; + times.cycle = mean * bench.count; + times.period = mean; + } + } + // If time permits, increase sample size to reduce the margin of error. + if (queue.length < 2 && !maxedOut) { + enqueue(); + } + // Abort the `invoke` cycle when done. + event.aborted = done; + } + + // Init queue and begin. + enqueue(); + invoke(queue, { + 'name': 'run', + 'args': { 'async': async }, + 'queued': true, + 'onCycle': evaluate, + 'onComplete': function() { bench.emit('complete'); } + }); + } + + /*------------------------------------------------------------------------*/ + + /** + * Cycles a benchmark until a run `count` can be established. + * + * @private + * @param {Object} clone The cloned benchmark instance. + * @param {Object} options The options object. + */ + function cycle(clone, options) { + options || (options = {}); + + var deferred; + if (clone instanceof Deferred) { + deferred = clone; + clone = clone.benchmark; + } + var clocked, + cycles, + divisor, + event, + minTime, + period, + async = options.async, + bench = clone._original, + count = clone.count, + times = clone.times; + + // Continue, if not aborted between cycles. + if (clone.running) { + // `minTime` is set to `Benchmark.options.minTime` in `clock()`. + cycles = ++clone.cycles; + clocked = deferred ? deferred.elapsed : clock(clone); + minTime = clone.minTime; + + if (cycles > bench.cycles) { + bench.cycles = cycles; + } + if (clone.error) { + event = Event('error'); + event.message = clone.error; + clone.emit(event); + if (!event.cancelled) { + clone.abort(); + } + } + } + // Continue, if not errored. + if (clone.running) { + // Compute the time taken to complete last test cycle. + bench.times.cycle = times.cycle = clocked; + // Compute the seconds per operation. + period = bench.times.period = times.period = clocked / count; + // Compute the ops per second. + bench.hz = clone.hz = 1 / period; + // Avoid working our way up to this next time. + bench.initCount = clone.initCount = count; + // Do we need to do another cycle? + clone.running = clocked < minTime; + + if (clone.running) { + // Tests may clock at `0` when `initCount` is a small number, + // to avoid that we set its count to something a bit higher. + if (!clocked && (divisor = divisors[clone.cycles]) != null) { + count = floor(4e6 / divisor); + } + // Calculate how many more iterations it will take to achieve the `minTime`. + if (count <= clone.count) { + count += Math.ceil((minTime - clocked) / period); + } + clone.running = count != Infinity; + } + } + // Should we exit early? + event = Event('cycle'); + clone.emit(event); + if (event.aborted) { + clone.abort(); + } + // Figure out what to do next. + if (clone.running) { + // Start a new cycle. + clone.count = count; + if (deferred) { + clone.compiled.call(deferred, context, timer); + } else if (async) { + delay(clone, function() { cycle(clone, options); }); + } else { + cycle(clone); + } + } + else { + // Fix TraceMonkey bug associated with clock fallbacks. + // For more information see http://bugzil.la/509069. + if (support.browser) { + runScript(uid + '=1;delete ' + uid); + } + // We're done. + clone.emit('complete'); + } + } + + /*------------------------------------------------------------------------*/ + + /** + * Runs the benchmark. + * + * @memberOf Benchmark + * @param {Object} [options={}] Options object. + * @returns {Object} The benchmark instance. + * @example + * + * // basic usage + * bench.run(); + * + * // or with options + * bench.run({ 'async': true }); + */ + function run(options) { + var bench = this, + event = Event('start'); + + // Set `running` to `false` so `reset()` won't call `abort()`. + bench.running = false; + bench.reset(); + bench.running = true; + + bench.count = bench.initCount; + bench.times.timeStamp = _.now(); + bench.emit(event); + + if (!event.cancelled) { + options = { 'async': ((options = options && options.async) == null ? bench.async : options) && support.timeout }; + + // For clones created within `compute()`. + if (bench._original) { + if (bench.defer) { + Deferred(bench); + } else { + cycle(bench, options); + } + } + // For original benchmarks. + else { + compute(bench, options); + } + } + return bench; + } + + /*------------------------------------------------------------------------*/ + + // Firefox 1 erroneously defines variable and argument names of functions on + // the function itself as non-configurable properties with `undefined` values. + // The bugginess continues as the `Benchmark` constructor has an argument + // named `options` and Firefox 1 will not assign a value to `Benchmark.options`, + // making it non-writable in the process, unless it is the first property + // assigned by for-in loop of `_.assign()`. + _.assign(Benchmark, { + + /** + * The default options copied by benchmark instances. + * + * @static + * @memberOf Benchmark + * @type Object + */ + 'options': { + + /** + * A flag to indicate that benchmark cycles will execute asynchronously + * by default. + * + * @memberOf Benchmark.options + * @type boolean + */ + 'async': false, + + /** + * A flag to indicate that the benchmark clock is deferred. + * + * @memberOf Benchmark.options + * @type boolean + */ + 'defer': false, + + /** + * The delay between test cycles (secs). + * @memberOf Benchmark.options + * @type number + */ + 'delay': 0.005, + + /** + * Displayed by `Benchmark#toString` when a `name` is not available + * (auto-generated if absent). + * + * @memberOf Benchmark.options + * @type string + */ + 'id': undefined, + + /** + * The default number of times to execute a test on a benchmark's first cycle. + * + * @memberOf Benchmark.options + * @type number + */ + 'initCount': 1, + + /** + * The maximum time a benchmark is allowed to run before finishing (secs). + * + * Note: Cycle delays aren't counted toward the maximum time. + * + * @memberOf Benchmark.options + * @type number + */ + 'maxTime': 5, + + /** + * The minimum sample size required to perform statistical analysis. + * + * @memberOf Benchmark.options + * @type number + */ + 'minSamples': 5, + + /** + * The time needed to reduce the percent uncertainty of measurement to 1% (secs). + * + * @memberOf Benchmark.options + * @type number + */ + 'minTime': 0, + + /** + * The name of the benchmark. + * + * @memberOf Benchmark.options + * @type string + */ + 'name': undefined, + + /** + * An event listener called when the benchmark is aborted. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onAbort': undefined, + + /** + * An event listener called when the benchmark completes running. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onComplete': undefined, + + /** + * An event listener called after each run cycle. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onCycle': undefined, + + /** + * An event listener called when a test errors. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onError': undefined, + + /** + * An event listener called when the benchmark is reset. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onReset': undefined, + + /** + * An event listener called when the benchmark starts running. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onStart': undefined + }, + + /** + * Platform object with properties describing things like browser name, + * version, and operating system. See [`platform.js`](https://mths.be/platform). + * + * @static + * @memberOf Benchmark + * @type Object + */ + 'platform': context.platform || require('platform') || ({ + 'description': context.navigator && context.navigator.userAgent || null, + 'layout': null, + 'product': null, + 'name': null, + 'manufacturer': null, + 'os': null, + 'prerelease': null, + 'version': null, + 'toString': function() { + return this.description || ''; + } + }), + + /** + * The semantic version number. + * + * @static + * @memberOf Benchmark + * @type string + */ + 'version': '2.1.2' + }); + + _.assign(Benchmark, { + 'filter': filter, + 'formatNumber': formatNumber, + 'invoke': invoke, + 'join': join, + 'runInContext': runInContext, + 'support': support + }); + + // Add lodash methods to Benchmark. + _.each(['each', 'forEach', 'forOwn', 'has', 'indexOf', 'map', 'reduce'], function(methodName) { + Benchmark[methodName] = _[methodName]; + }); + + /*------------------------------------------------------------------------*/ + + _.assign(Benchmark.prototype, { + + /** + * The number of times a test was executed. + * + * @memberOf Benchmark + * @type number + */ + 'count': 0, + + /** + * The number of cycles performed while benchmarking. + * + * @memberOf Benchmark + * @type number + */ + 'cycles': 0, + + /** + * The number of executions per second. + * + * @memberOf Benchmark + * @type number + */ + 'hz': 0, + + /** + * The compiled test function. + * + * @memberOf Benchmark + * @type {Function|string} + */ + 'compiled': undefined, + + /** + * The error object if the test failed. + * + * @memberOf Benchmark + * @type Object + */ + 'error': undefined, + + /** + * The test to benchmark. + * + * @memberOf Benchmark + * @type {Function|string} + */ + 'fn': undefined, + + /** + * A flag to indicate if the benchmark is aborted. + * + * @memberOf Benchmark + * @type boolean + */ + 'aborted': false, + + /** + * A flag to indicate if the benchmark is running. + * + * @memberOf Benchmark + * @type boolean + */ + 'running': false, + + /** + * Compiled into the test and executed immediately **before** the test loop. + * + * @memberOf Benchmark + * @type {Function|string} + * @example + * + * // basic usage + * var bench = Benchmark({ + * 'setup': function() { + * var c = this.count, + * element = document.getElementById('container'); + * while (c--) { + * element.appendChild(document.createElement('div')); + * } + * }, + * 'fn': function() { + * element.removeChild(element.lastChild); + * } + * }); + * + * // compiles to something like: + * var c = this.count, + * element = document.getElementById('container'); + * while (c--) { + * element.appendChild(document.createElement('div')); + * } + * var start = new Date; + * while (count--) { + * element.removeChild(element.lastChild); + * } + * var end = new Date - start; + * + * // or using strings + * var bench = Benchmark({ + * 'setup': '\ + * var a = 0;\n\ + * (function() {\n\ + * (function() {\n\ + * (function() {', + * 'fn': 'a += 1;', + * 'teardown': '\ + * }())\n\ + * }())\n\ + * }())' + * }); + * + * // compiles to something like: + * var a = 0; + * (function() { + * (function() { + * (function() { + * var start = new Date; + * while (count--) { + * a += 1; + * } + * var end = new Date - start; + * }()) + * }()) + * }()) + */ + 'setup': _.noop, + + /** + * Compiled into the test and executed immediately **after** the test loop. + * + * @memberOf Benchmark + * @type {Function|string} + */ + 'teardown': _.noop, + + /** + * An object of stats including mean, margin or error, and standard deviation. + * + * @memberOf Benchmark + * @type Object + */ + 'stats': { + + /** + * The margin of error. + * + * @memberOf Benchmark#stats + * @type number + */ + 'moe': 0, + + /** + * The relative margin of error (expressed as a percentage of the mean). + * + * @memberOf Benchmark#stats + * @type number + */ + 'rme': 0, + + /** + * The standard error of the mean. + * + * @memberOf Benchmark#stats + * @type number + */ + 'sem': 0, + + /** + * The sample standard deviation. + * + * @memberOf Benchmark#stats + * @type number + */ + 'deviation': 0, + + /** + * The sample arithmetic mean (secs). + * + * @memberOf Benchmark#stats + * @type number + */ + 'mean': 0, + + /** + * The array of sampled periods. + * + * @memberOf Benchmark#stats + * @type Array + */ + 'sample': [], + + /** + * The sample variance. + * + * @memberOf Benchmark#stats + * @type number + */ + 'variance': 0 + }, + + /** + * An object of timing data including cycle, elapsed, period, start, and stop. + * + * @memberOf Benchmark + * @type Object + */ + 'times': { + + /** + * The time taken to complete the last cycle (secs). + * + * @memberOf Benchmark#times + * @type number + */ + 'cycle': 0, + + /** + * The time taken to complete the benchmark (secs). + * + * @memberOf Benchmark#times + * @type number + */ + 'elapsed': 0, + + /** + * The time taken to execute the test once (secs). + * + * @memberOf Benchmark#times + * @type number + */ + 'period': 0, + + /** + * A timestamp of when the benchmark started (ms). + * + * @memberOf Benchmark#times + * @type number + */ + 'timeStamp': 0 + } + }); + + _.assign(Benchmark.prototype, { + 'abort': abort, + 'clone': clone, + 'compare': compare, + 'emit': emit, + 'listeners': listeners, + 'off': off, + 'on': on, + 'reset': reset, + 'run': run, + 'toString': toStringBench + }); + + /*------------------------------------------------------------------------*/ + + _.assign(Deferred.prototype, { + + /** + * The deferred benchmark instance. + * + * @memberOf Benchmark.Deferred + * @type Object + */ + 'benchmark': null, + + /** + * The number of deferred cycles performed while benchmarking. + * + * @memberOf Benchmark.Deferred + * @type number + */ + 'cycles': 0, + + /** + * The time taken to complete the deferred benchmark (secs). + * + * @memberOf Benchmark.Deferred + * @type number + */ + 'elapsed': 0, + + /** + * A timestamp of when the deferred benchmark started (ms). + * + * @memberOf Benchmark.Deferred + * @type number + */ + 'timeStamp': 0 + }); + + _.assign(Deferred.prototype, { + 'resolve': resolve + }); + + /*------------------------------------------------------------------------*/ + + _.assign(Event.prototype, { + + /** + * A flag to indicate if the emitters listener iteration is aborted. + * + * @memberOf Benchmark.Event + * @type boolean + */ + 'aborted': false, + + /** + * A flag to indicate if the default action is cancelled. + * + * @memberOf Benchmark.Event + * @type boolean + */ + 'cancelled': false, + + /** + * The object whose listeners are currently being processed. + * + * @memberOf Benchmark.Event + * @type Object + */ + 'currentTarget': undefined, + + /** + * The return value of the last executed listener. + * + * @memberOf Benchmark.Event + * @type Mixed + */ + 'result': undefined, + + /** + * The object to which the event was originally emitted. + * + * @memberOf Benchmark.Event + * @type Object + */ + 'target': undefined, + + /** + * A timestamp of when the event was created (ms). + * + * @memberOf Benchmark.Event + * @type number + */ + 'timeStamp': 0, + + /** + * The event type. + * + * @memberOf Benchmark.Event + * @type string + */ + 'type': '' + }); + + /*------------------------------------------------------------------------*/ + + /** + * The default options copied by suite instances. + * + * @static + * @memberOf Benchmark.Suite + * @type Object + */ + Suite.options = { + + /** + * The name of the suite. + * + * @memberOf Benchmark.Suite.options + * @type string + */ + 'name': undefined + }; + + /*------------------------------------------------------------------------*/ + + _.assign(Suite.prototype, { + + /** + * The number of benchmarks in the suite. + * + * @memberOf Benchmark.Suite + * @type number + */ + 'length': 0, + + /** + * A flag to indicate if the suite is aborted. + * + * @memberOf Benchmark.Suite + * @type boolean + */ + 'aborted': false, + + /** + * A flag to indicate if the suite is running. + * + * @memberOf Benchmark.Suite + * @type boolean + */ + 'running': false + }); + + _.assign(Suite.prototype, { + 'abort': abortSuite, + 'add': add, + 'clone': cloneSuite, + 'emit': emit, + 'filter': filterSuite, + 'join': arrayRef.join, + 'listeners': listeners, + 'off': off, + 'on': on, + 'pop': arrayRef.pop, + 'push': push, + 'reset': resetSuite, + 'run': runSuite, + 'reverse': arrayRef.reverse, + 'shift': shift, + 'slice': slice, + 'sort': arrayRef.sort, + 'splice': arrayRef.splice, + 'unshift': unshift + }); + + /*------------------------------------------------------------------------*/ + + // Expose Deferred, Event, and Suite. + _.assign(Benchmark, { + 'Deferred': Deferred, + 'Event': Event, + 'Suite': Suite + }); + + /*------------------------------------------------------------------------*/ + + // Add lodash methods as Suite methods. + _.each(['each', 'forEach', 'indexOf', 'map', 'reduce'], function(methodName) { + var func = _[methodName]; + Suite.prototype[methodName] = function() { + var args = [this]; + push.apply(args, arguments); + return func.apply(_, args); + }; + }); + + // Avoid array-like object bugs with `Array#shift` and `Array#splice` + // in Firefox < 10 and IE < 9. + _.each(['pop', 'shift', 'splice'], function(methodName) { + var func = arrayRef[methodName]; + + Suite.prototype[methodName] = function() { + var value = this, + result = func.apply(value, arguments); + + if (value.length === 0) { + delete value[0]; + } + return result; + }; + }); + + // Avoid buggy `Array#unshift` in IE < 8 which doesn't return the new + // length of the array. + Suite.prototype.unshift = function() { + var value = this; + unshift.apply(value, arguments); + return value.length; + }; + + return Benchmark; + } + + /*--------------------------------------------------------------------------*/ + + var Benchmark = runInContext(); + + window.Benchmark = Benchmark; + + return Benchmark; + +}.call(this)); diff --git a/tgui/packages/tgui-bench/package.json b/tgui/packages/tgui-bench/package.json new file mode 100644 index 00000000000..ae83ca86662 --- /dev/null +++ b/tgui/packages/tgui-bench/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "name": "tgui-bench", + "version": "4.3.0", + "dependencies": { + "@fastify/static": "^5.0.2", + "common": "workspace:*", + "fastify": "^3.20.2", + "inferno": "^7.4.8", + "inferno-vnode-flags": "^7.4.8", + "lodash": "^4.17.21", + "platform": "^1.3.6", + "tgui": "workspace:*" + } +} diff --git a/tgui/packages/tgui-bench/tests/Button.test.tsx b/tgui/packages/tgui-bench/tests/Button.test.tsx new file mode 100644 index 00000000000..e3472cbbbfa --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Button.test.tsx @@ -0,0 +1,99 @@ +import { linkEvent } from 'inferno'; +import { Button } from 'tgui/components'; +import { createRenderer } from 'tgui/renderer'; + +const render = createRenderer(); + +const handleClick = () => undefined; + +export const SingleButton = () => { + const node = ( + + ); + render(node); +}; + +export const SingleButtonWithCallback = () => { + const node = ( + + ); + render(node); +}; + +export const SingleButtonWithLinkEvent = () => { + const node = ( + + ); + render(node); +}; + +export const ListOfButtons = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithCallback = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithLinkEvent = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithIcons = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithTooltips = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; diff --git a/tgui/packages/tgui-bench/tests/DisposalBin.test.tsx b/tgui/packages/tgui-bench/tests/DisposalBin.test.tsx new file mode 100644 index 00000000000..99367d2016c --- /dev/null +++ b/tgui/packages/tgui-bench/tests/DisposalBin.test.tsx @@ -0,0 +1,28 @@ +import { backendUpdate } from 'tgui/backend'; +import { DisposalBin } from 'tgui/interfaces/DisposalBin'; +import { createRenderer } from 'tgui/renderer'; +import { configureStore, StoreProvider } from 'tgui/store'; + +const store = configureStore({ sideEffets: false }); + +const renderUi = createRenderer((dataJson: string) => { + store.dispatch(backendUpdate({ + data: Byond.parseJson(dataJson), + })); + return ( + + + + ); +}); + +export const data = JSON.stringify({ + flush: 0, + full_pressure: 1, + pressure_charging: 0, + panel_open: 0, + per: 1, + isai: 0, +}); + +export const Default = () => renderUi(data); diff --git a/tgui/packages/tgui-bench/tests/Flex.test.tsx b/tgui/packages/tgui-bench/tests/Flex.test.tsx new file mode 100644 index 00000000000..e81ebc6f611 --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Flex.test.tsx @@ -0,0 +1,18 @@ +import { Flex } from 'tgui/components'; +import { createRenderer } from 'tgui/renderer'; + +const render = createRenderer(); + +export const Default = () => { + const node = ( + + + Text {Math.random()} + + + Text {Math.random()} + + + ); + render(node); +}; diff --git a/tgui/packages/tgui-bench/tests/Stack.test.tsx b/tgui/packages/tgui-bench/tests/Stack.test.tsx new file mode 100644 index 00000000000..952dba20294 --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Stack.test.tsx @@ -0,0 +1,18 @@ +import { Stack } from 'tgui/components'; +import { createRenderer } from 'tgui/renderer'; + +const render = createRenderer(); + +export const Default = () => { + const node = ( + + + Text {Math.random()} + + + Text {Math.random()} + + + ); + render(node); +}; diff --git a/tgui/packages/tgui-bench/tests/Tooltip.test.tsx b/tgui/packages/tgui-bench/tests/Tooltip.test.tsx new file mode 100644 index 00000000000..b953fc911d2 --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Tooltip.test.tsx @@ -0,0 +1,20 @@ +import { Box, Tooltip } from "tgui/components"; +import { createRenderer } from "tgui/renderer"; + +const render = createRenderer(); + +export const ListOfTooltips = () => { + const nodes: JSX.Element[] = []; + + for (let i = 0; i < 100; i++) { + nodes.push( + + + Tooltip #{i} + + + ); + } + + render(
{nodes}
); +}; diff --git a/tgui/packages/tgui-polyfill/01-ie8.js b/tgui/packages/tgui-polyfill/01-ie8.js index af4b3a5ee85..3da4e2393d3 100644 --- a/tgui/packages/tgui-polyfill/01-ie8.js +++ b/tgui/packages/tgui-polyfill/01-ie8.js @@ -743,7 +743,7 @@ ontype = 'on' + type, handlers = (window[ontype] || Object)[SECRET], i = handlers ? find(handlers, handler) : -1 - ; + ; if (-1 < i) handlers.splice(i, 1); }), pageXOffset: {get: getter('scrollLeft')}, diff --git a/tgui/packages/tgui/components/Autofocus.tsx b/tgui/packages/tgui/components/Autofocus.tsx new file mode 100644 index 00000000000..f5ae00650aa --- /dev/null +++ b/tgui/packages/tgui/components/Autofocus.tsx @@ -0,0 +1,19 @@ +import { Component, createRef } from "inferno"; + +export class Autofocus extends Component { + ref = createRef(); + + componentDidMount() { + setTimeout(() => { + this.ref.current?.focus(); + }, 1); + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} diff --git a/tgui/packages/tgui/components/Box.tsx b/tgui/packages/tgui/components/Box.tsx index 015d07b23fd..bcd8447c8b2 100644 --- a/tgui/packages/tgui/components/Box.tsx +++ b/tgui/packages/tgui/components/Box.tsx @@ -5,7 +5,7 @@ */ import { BooleanLike, classes, pureComponentHooks } from 'common/react'; -import { createVNode, InfernoNode } from 'inferno'; +import { createVNode, InfernoNode, SFC } from 'inferno'; import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags'; import { CSS_COLORS } from '../constants'; @@ -109,9 +109,7 @@ export const halfUnit = (value: unknown): string | undefined => { const isColorCode = (str: unknown) => !isColorClass(str); const isColorClass = (str: unknown): boolean => { - if (typeof str === 'string') { - return CSS_COLORS.includes(str); - } + return typeof str === "string" && CSS_COLORS.includes(str); }; const mapRawPropTo = attrName => (style, value) => { @@ -290,7 +288,7 @@ export const computeBoxClassName = (props: BoxProps) => { ]); }; -export const Box = (props: BoxProps) => { +export const Box: SFC = (props: BoxProps) => { const { as = 'div', className, @@ -312,7 +310,9 @@ export const Box = (props: BoxProps) => { computedClassName, children, ChildFlags.UnknownChildren, - computedProps); + computedProps, + undefined, + ); }; Box.defaultHooks = pureComponentHooks; diff --git a/tgui/packages/tgui/components/Button.js b/tgui/packages/tgui/components/Button.js index 6c5d1736b33..58c3dc76970 100644 --- a/tgui/packages/tgui/components/Button.js +++ b/tgui/packages/tgui/components/Button.js @@ -8,7 +8,7 @@ import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from 'common/keycodes'; import { classes, pureComponentHooks } from 'common/react'; import { Component, createRef } from 'inferno'; import { createLogger } from '../logging'; -import { Box } from './Box'; +import { Box, computeBoxClassName, computeBoxProps } from './Box'; import { Icon } from './Icon'; import { Tooltip } from './Tooltip'; @@ -47,10 +47,17 @@ export const Button = props => { + `'onClick' instead and read: ` + `https://infernojs.org/docs/guides/event-handling`); } - // IE8: Use a lowercase "onclick" because synthetic events are fucked. - // IE8: Use an "unselectable" prop because "user-select" doesn't work. + rest.onClick = e => { + if (!disabled && onClick) { + onClick(e); + } + }; + // IE8: Use "unselectable" because "user-select" doesn't work. + if (Byond.IS_LTE_IE8) { + rest.unselectable = true; + } let buttonContent = ( - { ? 'Button--color--' + color : 'Button--color--default', className, + computeBoxClassName(rest), ])} tabIndex={!disabled && '0'} - unselectable={Byond.IS_LTE_IE8} - onClick={e => { - if (!disabled && onClick) { - onClick(e); - } - }} onKeyDown={e => { + if (props.captureKeys === false) { + return; + } + const keyCode = window.event ? e.which : e.keyCode; // Simulate a click when pressing space or enter. if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) { @@ -89,7 +95,7 @@ export const Button = props => { return; } }} - {...rest}> + {...computeBoxProps(rest)}> {(icon && iconPosition !== 'right') && ( { fontSize={iconSize} // VOREStation Addition /> )} - + ); if (tooltip) { diff --git a/tgui/packages/tgui/components/ByondUi.js b/tgui/packages/tgui/components/ByondUi.js index 43d4039fcc2..5b4f04dd0e5 100644 --- a/tgui/packages/tgui/components/ByondUi.js +++ b/tgui/packages/tgui/components/ByondUi.js @@ -133,11 +133,10 @@ export class ByondUi extends Component { render() { const { params, ...rest } = this.props; - const boxProps = computeBoxProps(rest); return (
+ {...computeBoxProps(rest)}> {/* Filler */}
diff --git a/tgui/packages/tgui/components/Flex.tsx b/tgui/packages/tgui/components/Flex.tsx index 172fcad93d8..95379ea587e 100644 --- a/tgui/packages/tgui/components/Flex.tsx +++ b/tgui/packages/tgui/components/Flex.tsx @@ -5,16 +5,26 @@ */ import { BooleanLike, classes, pureComponentHooks } from 'common/react'; -import { Box, BoxProps, unit } from './Box'; +import { BoxProps, computeBoxClassName, computeBoxProps, unit } from './Box'; -export interface FlexProps extends BoxProps { +export type FlexProps = BoxProps & { direction?: string | BooleanLike; wrap?: string | BooleanLike; align?: string | BooleanLike; alignContent?: string | BooleanLike; // VOREStation Addition justify?: string | BooleanLike; inline?: BooleanLike; -} +}; + +export const computeFlexClassName = (props: FlexProps) => { + return classes([ + 'Flex', + props.inline && 'Flex--inline', + Byond.IS_LTE_IE10 && 'Flex--iefix', + Byond.IS_LTE_IE10 && props.direction === 'column' && 'Flex--iefix--column', + computeBoxClassName(props), + ]); +}; export const computeFlexProps = (props: FlexProps) => { const { @@ -27,17 +37,7 @@ export const computeFlexProps = (props: FlexProps) => { inline, ...rest } = props; - return { - className: classes([ - 'Flex', - Byond.IS_LTE_IE10 && ( - direction === 'column' - ? 'Flex--iefix--column' - : 'Flex--iefix' - ), - inline && 'Flex--inline', - className, - ]), + return computeBoxProps({ style: { ...rest.style, 'flex-direction': direction, @@ -47,22 +47,39 @@ export const computeFlexProps = (props: FlexProps) => { 'justify-content': justify, }, ...rest, - }; + }); }; -export const Flex = props => ( - -); +export const Flex = props => { + const { className, ...rest } = props; + return ( +
+ ); +}; Flex.defaultHooks = pureComponentHooks; -export interface FlexItemProps extends BoxProps { +export type FlexItemProps = BoxProps & { grow?: number; order?: number; shrink?: number; basis?: string | BooleanLike; align?: string | BooleanLike; -} +}; + +export const computeFlexItemClassName = (props: FlexItemProps) => { + return classes([ + 'Flex__item', + Byond.IS_LTE_IE10 && 'Flex__item--iefix', + computeBoxClassName(props), + ]); +}; export const computeFlexItemProps = (props: FlexItemProps) => { const { @@ -77,13 +94,7 @@ export const computeFlexItemProps = (props: FlexItemProps) => { align, ...rest } = props; - return { - className: classes([ - 'Flex__item', - Byond.IS_LTE_IE10 && 'Flex__item--iefix', - Byond.IS_LTE_IE10 && grow > 0 && 'Flex__item--iefix--grow', - className, - ]), + return computeBoxProps({ style: { ...style, 'flex-grow': grow !== undefined && Number(grow), @@ -93,12 +104,22 @@ export const computeFlexItemProps = (props: FlexItemProps) => { 'align-self': align, }, ...rest, - }; + }); }; -const FlexItem = props => ( - -); +const FlexItem = props => { + const { className, ...rest } = props; + return ( +
+ ); +}; FlexItem.defaultHooks = pureComponentHooks; diff --git a/tgui/packages/tgui/components/Icon.js b/tgui/packages/tgui/components/Icon.js index 13efaffca79..98a40bf5d94 100644 --- a/tgui/packages/tgui/components/Icon.js +++ b/tgui/packages/tgui/components/Icon.js @@ -7,7 +7,7 @@ */ import { classes, pureComponentHooks } from 'common/react'; -import { Box } from './Box'; +import { computeBoxClassName, computeBoxProps } from './Box'; const FA_OUTLINE_REGEX = /-o$/; @@ -17,17 +17,26 @@ export const Icon = props => { size, spin, className, - style = {}, rotation, inverse, ...rest } = props; + if (size) { - style['font-size'] = (size * 100) + '%'; + if (!rest.style) { + rest.style = {}; + } + rest.style['font-size'] = (size * 100) + '%'; } if (typeof rotation === 'number') { - style['transform'] = `rotate(${rotation}deg)`; + if (!rest.style) { + rest.style = {}; + } + rest.style['transform'] = `rotate(${rotation}deg)`; } + + const boxProps = computeBoxProps(rest); + let iconClass = ""; if (name.startsWith("tg-")) { // tgfont icon @@ -39,15 +48,14 @@ export const Icon = props => { iconClass = (faRegular ? 'far ' : 'fas ') + 'fa-'+ faName + (spin ? " fa-spin" : ""); } return ( - + {...boxProps} /> ); }; @@ -56,21 +64,19 @@ Icon.defaultHooks = pureComponentHooks; export const IconStack = props => { const { className, - style = {}, children, ...rest } = props; return ( - + {...computeBoxProps(rest)}> {children} - + ); }; diff --git a/tgui/packages/tgui/components/Input.js b/tgui/packages/tgui/components/Input.js index e09c015d3e6..a95dbab614c 100644 --- a/tgui/packages/tgui/components/Input.js +++ b/tgui/packages/tgui/components/Input.js @@ -73,6 +73,11 @@ export class Input extends Component { return; } if (e.keyCode === KEY_ESCAPE) { + if (this.props.onEscape) { + this.props.onEscape(e); + return; + } + this.setEditing(false); e.target.value = toInputValue(this.props.value); e.target.blur(); @@ -87,8 +92,15 @@ export class Input extends Component { if (input) { input.value = toInputValue(nextValue); } - if (this.props.autoFocus) { - setTimeout(() => input.focus(), 1); + + if (this.props.autoFocus || this.props.autoSelect) { + setTimeout(() => { + input.focus(); + + if (this.props.autoSelect) { + input.select(); + } + }, 1); } } diff --git a/tgui/packages/tgui/components/KeyListener.tsx b/tgui/packages/tgui/components/KeyListener.tsx new file mode 100644 index 00000000000..46b52765365 --- /dev/null +++ b/tgui/packages/tgui/components/KeyListener.tsx @@ -0,0 +1,39 @@ +import { Component } from "inferno"; +import { KeyEvent } from "../events"; +import { listenForKeyEvents } from "../hotkeys"; + +type KeyListenerProps = Partial<{ + onKey: (key: KeyEvent) => void, + onKeyDown: (key: KeyEvent) => void, + onKeyUp: (key: KeyEvent) => void, +}>; + +export class KeyListener extends Component { + dispose: () => void; + + constructor() { + super(); + + this.dispose = listenForKeyEvents(key => { + if (this.props.onKey) { + this.props.onKey(key); + } + + if (key.isDown() && this.props.onKeyDown) { + this.props.onKeyDown(key); + } + + if (key.isUp() && this.props.onKeyUp) { + this.props.onKeyUp(key); + } + }); + } + + componentWillUnmount() { + this.dispose(); + } + + render() { + return null; + } +} diff --git a/tgui/packages/tgui/components/Popper.tsx b/tgui/packages/tgui/components/Popper.tsx index f5a50f64330..f288dc67e7d 100644 --- a/tgui/packages/tgui/components/Popper.tsx +++ b/tgui/packages/tgui/components/Popper.tsx @@ -1,10 +1,10 @@ -// import { createPopper, OptionsGeneric } from "@popperjs/core"; -import { createPopper, OptionsGeneric } from '@popperjs/core'; +import { createPopper } from '@popperjs/core'; +import { ArgumentsOf } from 'common/types'; import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno'; type PopperProps = { popperContent: InfernoNode; - options?: Partial>; + options?: ArgumentsOf[2]; additionalStyles?: CSSProperties; }; @@ -33,17 +33,22 @@ export class Popper extends Component { this.renderPopperContent(() => { document.body.appendChild(this.renderedContent); + // HACK: We don't want to create a wrapper, as it could break the layout + // of consumers, so we do the inferno equivalent of `findDOMNode(this)`. + // This is usually bad as refs are usually better, but refs did + // not work in this case, as they weren't propagating correctly. + // A previous attempt was made as a render prop that passed an ID, + // but this made consuming use too unwieldly. + // This code is copied from `findDOMNode` in inferno-extras. + // Because this component is written in TypeScript, we will know + // immediately if this internal variable is removed. + const domNode = findDOMfromVNode(this.$LI, true); + if (!domNode) { + return; + } + this.popperInstance = createPopper( - // HACK: We don't want to create a wrapper, as it could break the layout - // of consumers, so we do the inferno equivalent of `findDOMNode(this)`. - // This is usually bad as refs are usually better, but refs did - // not work in this case, as they weren't propagating correctly. - // A previous attempt was made as a render prop that passed an ID, - // but this made consuming use too unwieldly. - // This code is copied from `findDOMNode` in inferno-extras. - // Because this component is written in TypeScript, we will know - // immediately if this internal variable is removed. - findDOMfromVNode(this.$LI, true), + domNode, this.renderedContent, options ); @@ -56,12 +61,20 @@ export class Popper extends Component { componentWillUnmount() { this.popperInstance?.destroy(); - this.renderedContent.remove(); - this.renderedContent = null; + render(null, this.renderedContent, () => { + this.renderedContent.remove(); + }); } renderPopperContent(callback: () => void) { - render(this.props.popperContent, this.renderedContent, callback); + // `render` errors when given false, so we convert it to `null`, + // which is supported. + render( + this.props.popperContent || null, + this.renderedContent, + callback, + this.context, + ); } render() { diff --git a/tgui/packages/tgui/components/Section.tsx b/tgui/packages/tgui/components/Section.tsx index 43317fd43bd..d49043ee8ab 100644 --- a/tgui/packages/tgui/components/Section.tsx +++ b/tgui/packages/tgui/components/Section.tsx @@ -40,7 +40,11 @@ export class Section extends Component { addScrollableNode(this.scrollableRef.current); } if (this.props.autoFocus) { - setTimeout(() => this.scrollableRef.current.focus(), 1); + setTimeout(() => { + if (this.scrollableRef.current) { + return this.scrollableRef.current.focus(); + } + }, 1); } } diff --git a/tgui/packages/tgui/components/Stack.tsx b/tgui/packages/tgui/components/Stack.tsx index 1801c3a145e..66de1b25577 100644 --- a/tgui/packages/tgui/components/Stack.tsx +++ b/tgui/packages/tgui/components/Stack.tsx @@ -5,6 +5,7 @@ */ import { classes } from 'common/react'; +import { RefObject } from 'inferno'; import { Flex, FlexItemProps, FlexProps } from './Flex'; interface StackProps extends FlexProps { @@ -29,14 +30,19 @@ export const Stack = (props: StackProps) => { ); }; -const StackItem = (props: FlexProps) => { - const { className, ...rest } = props; +type StackItemProps = FlexProps & { + innerRef?: RefObject, +}; + +const StackItem = (props: StackItemProps) => { + const { className, innerRef, ...rest } = props; return ( ); }; diff --git a/tgui/packages/tgui/components/Tooltip.tsx b/tgui/packages/tgui/components/Tooltip.tsx index 7638ce07de5..0cf61ed04fc 100644 --- a/tgui/packages/tgui/components/Tooltip.tsx +++ b/tgui/packages/tgui/components/Tooltip.tsx @@ -7,7 +7,7 @@ const DEFAULT_PLACEMENT = "top"; type TooltipProps = { children?: InfernoNode; - content: string; + content: InfernoNode; position?: Placement, }; @@ -15,14 +15,15 @@ type TooltipState = { hovered: boolean; }; -export class Tooltip extends Component { - constructor() { - super(); +const DISABLE_EVENT_LISTENERS = [{ + name: "eventListeners", + enabled: false, +}]; - this.state = { - hovered: false, - }; - } +export class Tooltip extends Component { + state = { + hovered: false, + }; componentDidMount() { // HACK: We don't want to create a wrapper, as it could break the layout @@ -35,6 +36,10 @@ export class Tooltip extends Component { // immediately if this internal variable is removed. const domNode = findDOMfromVNode(this.$LI, true); + if (!domNode) { + return; + } + domNode.addEventListener("mouseenter", () => { this.setState({ hovered: true, @@ -53,6 +58,7 @@ export class Tooltip extends Component { { } additionalStyles={{ "pointer-events": "none", + "z-index": 2, }}> {this.props.children} diff --git a/tgui/packages/tgui/components/TrackOutsideClicks.tsx b/tgui/packages/tgui/components/TrackOutsideClicks.tsx new file mode 100644 index 00000000000..abf8286c7f0 --- /dev/null +++ b/tgui/packages/tgui/components/TrackOutsideClicks.tsx @@ -0,0 +1,37 @@ +import { Component, createRef } from "inferno"; + +export class TrackOutsideClicks extends Component<{ + onOutsideClick: () => void, +}> { + ref = createRef(); + + constructor() { + super(); + + this.handleOutsideClick = this.handleOutsideClick.bind(this); + + document.addEventListener("click", this.handleOutsideClick); + } + + componentWillUnmount() { + document.removeEventListener("click", this.handleOutsideClick); + } + + handleOutsideClick(event: MouseEvent) { + if (!(event.target instanceof Node)) { + return; + } + + if (this.ref.current && !this.ref.current.contains(event.target)) { + this.props.onOutsideClick(); + } + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} diff --git a/tgui/packages/tgui/components/index.js b/tgui/packages/tgui/components/index.js index 93457d12342..d27639a6c14 100644 --- a/tgui/packages/tgui/components/index.js +++ b/tgui/packages/tgui/components/index.js @@ -5,6 +5,7 @@ */ export { AnimatedNumber } from './AnimatedNumber'; +export { Autofocus } from './Autofocus'; export { Blink } from './Blink'; export { BlockQuote } from './BlockQuote'; export { Box } from './Box'; @@ -22,6 +23,7 @@ export { Grid } from './Grid'; export { Icon } from './Icon'; export { InfinitePlane } from './InfinitePlane'; export { Input } from './Input'; +export { KeyListener } from './KeyListener'; export { Knob } from './Knob'; export { LabeledControls } from './LabeledControls'; export { LabeledList } from './LabeledList'; @@ -39,4 +41,5 @@ export { Table } from './Table'; export { Tabs } from './Tabs'; export { TextArea } from './TextArea'; export { TimeDisplay } from './TimeDisplay'; +export { TrackOutsideClicks } from './TrackOutsideClicks'; export { Tooltip } from './Tooltip'; diff --git a/tgui/packages/tgui/hotkeys.ts b/tgui/packages/tgui/hotkeys.ts index 3334c892cb6..b11c8cfceea 100644 --- a/tgui/packages/tgui/hotkeys.ts +++ b/tgui/packages/tgui/hotkeys.ts @@ -31,6 +31,9 @@ const hotKeysAcquired = [ // State of passed-through keys. const keyState: Record = {}; +// Custom listeners for key events +const keyListeners: ((key: KeyEvent) => void)[] = []; + /** * Converts a browser keycode to BYOND keycode. */ @@ -178,6 +181,38 @@ export const setupHotKeys = () => { releaseHeldKeys(); }); globalEvents.on('key', (key: KeyEvent) => { + for (const keyListener of keyListeners) { + keyListener(key); + } + handlePassthrough(key); }); }; + +/** + * Registers for any key events, such as key down or key up. + * This should be preferred over directly connecting to keydown/keyup + * as it lets tgui prevent the key from reaching BYOND. + * + * If using in a component, prefer KeyListener, which automatically handles + * stopping listening when unmounting. + * + * @param callback The function to call whenever a key event occurs + * @returns A callback to stop listening + */ +export const listenForKeyEvents = ( + callback: (key: KeyEvent) => void, +): () => void => { + keyListeners.push(callback); + + let removed = false; + + return () => { + if (removed) { + return; + } + + removed = true; + keyListeners.splice(keyListeners.indexOf(callback), 1); + }; +}; diff --git a/tgui/packages/tgui/http.ts b/tgui/packages/tgui/http.ts new file mode 100644 index 00000000000..96a2d9b56df --- /dev/null +++ b/tgui/packages/tgui/http.ts @@ -0,0 +1,17 @@ +/** + * An equivalent to `fetch`, except will automatically retry. + */ +export const fetchRetry + = ( + url: string, + options?: RequestInit, + retryTimer: number = 1000, + ): Promise => { + return fetch(url, options).catch(() => { + return new Promise(resolve => { + setTimeout(() => { + fetchRetry(url, options, retryTimer).then(resolve); + }, retryTimer); + }); + }); + }; diff --git a/tgui/packages/tgui/interfaces/AlertModal.js b/tgui/packages/tgui/interfaces/AlertModal.js index c660c897869..2815bd93d54 100644 --- a/tgui/packages/tgui/interfaces/AlertModal.js +++ b/tgui/packages/tgui/interfaces/AlertModal.js @@ -4,7 +4,7 @@ * @license MIT */ -import { clamp01 } from 'common/math'; +import { Loader } from "./common/Loader"; import { useBackend } from '../backend'; import { Component, createRef } from 'inferno'; import { Box, Flex, Section } from '../components'; @@ -135,15 +135,3 @@ export class AlertModal extends Component { } } - -export const Loader = props => { - const { value } = props; - - return ( -
- -
- ); -}; diff --git a/tgui/packages/tgui/interfaces/ListInput.js b/tgui/packages/tgui/interfaces/ListInput.js index 77d03f06ed9..1d301a30286 100644 --- a/tgui/packages/tgui/interfaces/ListInput.js +++ b/tgui/packages/tgui/interfaces/ListInput.js @@ -4,9 +4,9 @@ * @license MIT */ -import { clamp01 } from 'common/math'; +import { Loader } from "./common/Loader"; import { useBackend, useLocalState } from '../backend'; -import { Box, Button, Section, Input, Stack } from '../components'; +import { Button, Section, Input, Stack } from '../components'; import { KEY_DOWN, KEY_UP, KEY_ENTER, KEY_SPACE, KEY_ESCAPE, KEY_HOME, KEY_END } from 'common/keycodes'; import { Window } from '../layouts'; @@ -210,16 +210,3 @@ export const ListInput = (props, context) => { ); }; - -export const Loader = props => { - const { value } = props; - return ( -
- -
- ); -}; diff --git a/tgui/packages/tgui/interfaces/RCON.js b/tgui/packages/tgui/interfaces/RCON.js index db435ac0707..70787280db7 100644 --- a/tgui/packages/tgui/interfaces/RCON.js +++ b/tgui/packages/tgui/interfaces/RCON.js @@ -14,7 +14,7 @@ export const RCON = (props, context) => { return ( @@ -61,10 +61,16 @@ const RCONSmesList = (props, context) => { const { smes_info, + pages, + current_page, } = data; + const runCallback = (cb) => { + return cb(); + }; + return ( -
+
{smes_info.map(smes => ( @@ -72,6 +78,23 @@ const RCONSmesList = (props, context) => { ))} + Page Selection:
+ {runCallback(() => { + const row = []; + for (let i = 1; i < pages; i++) { + row.push( + + ); + } + return row; + })}
); }; diff --git a/tgui/packages/tgui/interfaces/common/Loader.tsx b/tgui/packages/tgui/interfaces/common/Loader.tsx new file mode 100644 index 00000000000..65c7d11e066 --- /dev/null +++ b/tgui/packages/tgui/interfaces/common/Loader.tsx @@ -0,0 +1,14 @@ +import { Box } from '../../components'; +import { clamp01 } from 'common/math'; + +export const Loader = props => { + const { value } = props; + + return ( +
+ +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/pda/pda_messenger.js b/tgui/packages/tgui/interfaces/pda/pda_messenger.js index ed22e860706..664ebc7eebb 100644 --- a/tgui/packages/tgui/interfaces/pda/pda_messenger.js +++ b/tgui/packages/tgui/interfaces/pda/pda_messenger.js @@ -106,6 +106,10 @@ const ActiveConversation = (props, context) => { } height="450px" stretchContents> +