From 165bb939c7adde2b16bb03cf3c7134de16b034f9 Mon Sep 17 00:00:00 2001 From: ShadowLarkens Date: Wed, 16 Sep 2020 21:31:22 -0700 Subject: [PATCH] TGUI Uplink --- code/game/objects/items/devices/uplink.dm | 250 +++++++++-------- nano/templates/uplink.tmpl | 177 ------------ tgui/packages/tgui/interfaces/Uplink.js | 322 ++++++++++++++++++++++ 3 files changed, 450 insertions(+), 299 deletions(-) delete mode 100644 nano/templates/uplink.tmpl create mode 100644 tgui/packages/tgui/interfaces/Uplink.js diff --git a/code/game/objects/items/devices/uplink.dm b/code/game/objects/items/devices/uplink.dm index 5a19c72742..5b6e55290e 100644 --- a/code/game/objects/items/devices/uplink.dm +++ b/code/game/objects/items/devices/uplink.dm @@ -1,13 +1,23 @@ GLOBAL_LIST_BOILERPLATE(world_uplinks, /obj/item/device/uplink) +// I placed this here because of how relevant it is. +// You place this in your uplinkable item to check if an uplink is active or not. +// If it is, it will display the uplink menu and return 1, else it'll return false. +// If it returns true, I recommend closing the item's normal menu with "user << browse(null, "window=name")" +/obj/item/proc/active_uplink_check(mob/user as mob) + // Activates the uplink if it's active + if(hidden_uplink) + if(hidden_uplink.active) + hidden_uplink.trigger(user) + return TRUE + return FALSE + /obj/item/device/uplink var/welcome = "Welcome, Operative" // Welcoming menu message var/uses // Numbers of crystals var/list/ItemsCategory // List of categories with lists of items var/list/ItemsReference // List of references with an associated item var/list/nanoui_items // List of items for NanoUI use - var/nanoui_menu = 0 // The current menu we are in - var/list/nanoui_data = new // Additional data for NanoUI use var/faction = "" //Antag faction holder. var/list/purchase_log = new @@ -17,9 +27,7 @@ GLOBAL_LIST_BOILERPLATE(world_uplinks, /obj/item/device/uplink) var/next_offer_time var/datum/uplink_item/discount_item //The item to be discounted var/discount_amount //The amount as a percent the item will be discounted by - -/obj/item/device/uplink/nano_host() - return loc + var/compact_mode = FALSE /obj/item/device/uplink/Initialize(var/mapload, var/datum/mind/owner = null, var/telecrystals = DEFAULT_TELECRYSTAL_AMOUNT) . = ..() @@ -53,23 +61,20 @@ GLOBAL_LIST_BOILERPLATE(world_uplinks, /obj/item/device/uplink) name = "hidden uplink" desc = "There is something wrong if you're examining this." var/active = 0 - var/datum/uplink_category/category = 0 // The current category we are in var/exploit_id // Id of the current exploit record we are viewing + var/selected_cat // The hidden uplink MUST be inside an obj/item's contents. /obj/item/device/uplink/hidden/Initialize() . = ..() if(!isitem(loc)) return INITIALIZE_HINT_QDEL - nanoui_data = list() - update_nano_data() /obj/item/device/uplink/hidden/next_offer() discount_item = default_uplink_selection.get_random_item(INFINITY) discount_amount = pick(90;0.9, 80;0.8, 70;0.7, 60;0.6, 50;0.5, 40;0.4, 30;0.3, 20;0.2, 10;0.1) - update_nano_data() - SSnanoui.update_uis(src) next_offer_time = world.time + offer_time + SStgui.update_uis(src) addtimer(CALLBACK(src, .proc/next_offer), offer_time) // Toggles the uplink on and off. Normally this will bypass the item's normal functions and go to the uplink menu, if activated. @@ -91,136 +96,137 @@ GLOBAL_LIST_BOILERPLATE(world_uplinks, /obj/item/device/uplink) return 1 return 0 -/* - NANO UI FOR UPLINK WOOP WOOP -*/ -/obj/item/device/uplink/hidden/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1) - var/title = "Remote Uplink" - var/data[0] - uses = user.mind.tcrystals - if(ishuman(user)) - var/mob/living/carbon/human/H = user - faction = H.antag_faction +// Legacy +/obj/item/device/uplink/hidden/interact(mob/user) + tgui_interact(user) - data["welcome"] = welcome - data["crystals"] = uses - data["menu"] = nanoui_menu - data += nanoui_data +/***************** + * Uplink TGUI + *****************/ +/obj/item/device/uplink/tgui_host() + return loc - // update the ui if it exists, returns null if no ui is passed/found - ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open) - if (!ui) // No auto-refresh - ui = new(user, src, ui_key, "uplink.tmpl", title, 630, 700, state = inventory_state) - data["menu"] = 0 - ui.set_initial_data(data) +/obj/item/device/uplink/hidden/tgui_state(mob/user) + return GLOB.tgui_inventory_state + +/obj/item/device/uplink/hidden/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui) + if(!active) + toggle() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Uplink", "Remote Uplink") + // This UI is only ever opened by one person, + // and never is updated outside of user input. + ui.set_autoupdate(FALSE) ui.open() +/obj/item/device/uplink/hidden/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + if(!user.mind) + return -// Interaction code. Gathers a list of items purchasable from the paren't uplink and displays it. It also adds a lock button. -/obj/item/device/uplink/hidden/interact(mob/user) - ui_interact(user) + var/list/data = ..() -/obj/item/device/uplink/hidden/CanUseTopic() + data["telecrystals"] = uses + data["lockable"] = TRUE + data["compactMode"] = compact_mode + + data["discount_name"] = discount_item ? discount_item.name : "" + data["discount_amount"] = (1-discount_amount)*100 + data["offer_expiry"] = worldtime2stationtime(next_offer_time) + + data["exploit"] = null + data["locked_records"] = null + + if(exploit_id) + for(var/datum/data/record/L in data_core.locked) + if(L.fields["id"] == exploit_id) + data["exploit"] = list() // Setting this to equal L.fields passes it's variables that are lists as reference instead of value. + // We trade off being able to automatically add shit for more control over what gets passed to json + // and if it's sanitized for html. + data["exploit"]["nanoui_exploit_record"] = html_encode(L.fields["exploit_record"]) // Change stuff into html + data["exploit"]["nanoui_exploit_record"] = replacetext(data["exploit"]["nanoui_exploit_record"], "\n", "
") // change line breaks into
+ data["exploit"]["name"] = html_encode(L.fields["name"]) + data["exploit"]["sex"] = html_encode(L.fields["sex"]) + data["exploit"]["age"] = html_encode(L.fields["age"]) + data["exploit"]["species"] = html_encode(L.fields["species"]) + data["exploit"]["rank"] = html_encode(L.fields["rank"]) + data["exploit"]["home_system"] = html_encode(L.fields["home_system"]) + data["exploit"]["citizenship"] = html_encode(L.fields["citizenship"]) + data["exploit"]["faction"] = html_encode(L.fields["faction"]) + data["exploit"]["religion"] = html_encode(L.fields["religion"]) + data["exploit"]["fingerprint"] = html_encode(L.fields["fingerprint"]) + if(L.fields["antagvis"] == ANTAG_KNOWN || (faction == L.fields["antagfac"] && (L.fields["antagvis"] == ANTAG_SHARED))) + data["exploit"]["antagfaction"] = html_encode(L.fields["antagfac"]) + else + data["exploit"]["antagfaction"] = html_encode("None") + break + else + var/list/permanentData = list() + for(var/datum/data/record/L in sortRecord(data_core.locked)) + permanentData.Add(list(list( + "name" = L.fields["name"], + "id" = L.fields["id"] + ))) + data["locked_records"] = permanentData + + return data + +/obj/item/device/uplink/hidden/tgui_static_data(mob/user) + var/list/data = ..() + + data["categories"] = list() + for(var/datum/uplink_category/category in uplink.categories) + if(category.can_view(src)) + var/list/cat = list( + "name" = category.name, + "items" = (category == selected_cat ? list() : null) + ) + for(var/datum/uplink_item/item in category.items) + if(!item.can_view(src)) + continue + var/cost = item.cost(uses, src) || "???" + cat["items"] += list(list( + "name" = item.name, + "cost" = cost, + "desc" = item.description(), + "ref" = REF(item), + )) + data["categories"] += list(cat) + + return data + +/obj/item/device/uplink/hidden/tgui_status(mob/user, datum/tgui_state/state) if(!active) return STATUS_CLOSE return ..() -// The purchasing code. -/obj/item/device/uplink/hidden/Topic(href, href_list) +/obj/item/device/uplink/hidden/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) if(..()) - return 1 + return TRUE - var/mob/user = usr - if(href_list["buy_item"]) - var/datum/uplink_item/UI = (locate(href_list["buy_item"]) in uplink.items) - UI.buy(src, usr) - else if(href_list["lock"]) - toggle() - var/datum/nanoui/ui = SSnanoui.get_open_ui(user, src, "main") - ui.close() - else if(href_list["return"]) - nanoui_menu = round(nanoui_menu/10) - else if(href_list["menu"]) - nanoui_menu = text2num(href_list["menu"]) - if(href_list["id"]) - exploit_id = href_list["id"] - else if(href_list["category"]) - category = locate(href_list["category"]) in uplink.categories - - update_nano_data() - return 1 - -/obj/item/device/uplink/hidden/proc/update_nano_data() - if(nanoui_menu == 0) - var/categories[0] - for(var/datum/uplink_category/category in uplink.categories) - if(category.can_view(src)) - categories[++categories.len] = list("name" = category.name, "ref" = "\ref[category]") - nanoui_data["categories"] = categories - nanoui_data["discount_name"] = discount_item ? discount_item.name : "" - nanoui_data["discount_amount"] = (1-discount_amount)*100 - nanoui_data["offer_expiry"] = worldtime2stationtime(next_offer_time) - - if(category) - nanoui_data["current_category"] = category.name - var/items[0] - for(var/datum/uplink_item/item in category.items) - if(item.can_view(src)) - var/cost = item.cost(uses, src) - if(!cost) cost = "???" - items[++items.len] = list("name" = item.name, "description" = replacetext(item.description(), "\n", "
"), "can_buy" = item.can_buy(src), "cost" = cost, "ref" = "\ref[item]") - nanoui_data["items"] = items - - else if(nanoui_menu == 2) - var/permanentData[0] - for(var/datum/data/record/L in sortRecord(data_core.locked)) - permanentData[++permanentData.len] = list(Name = L.fields["name"],"id" = L.fields["id"]) - nanoui_data["exploit_records"] = permanentData - else if(nanoui_menu == 21) - nanoui_data["exploit_exists"] = 0 - - for(var/datum/data/record/L in data_core.locked) - if(L.fields["id"] == exploit_id) - nanoui_data["exploit"] = list() // Setting this to equal L.fields passes it's variables that are lists as reference instead of value. - // We trade off being able to automatically add shit for more control over what gets passed to json - // and if it's sanitized for html. - nanoui_data["exploit"]["nanoui_exploit_record"] = html_encode(L.fields["exploit_record"]) // Change stuff into html - nanoui_data["exploit"]["nanoui_exploit_record"] = replacetext(nanoui_data["exploit"]["nanoui_exploit_record"], "\n", "
") // change line breaks into
- nanoui_data["exploit"]["name"] = html_encode(L.fields["name"]) - nanoui_data["exploit"]["sex"] = html_encode(L.fields["sex"]) - nanoui_data["exploit"]["age"] = html_encode(L.fields["age"]) - nanoui_data["exploit"]["species"] = html_encode(L.fields["species"]) - nanoui_data["exploit"]["rank"] = html_encode(L.fields["rank"]) - nanoui_data["exploit"]["home_system"] = html_encode(L.fields["home_system"]) - nanoui_data["exploit"]["citizenship"] = html_encode(L.fields["citizenship"]) - nanoui_data["exploit"]["faction"] = html_encode(L.fields["faction"]) - nanoui_data["exploit"]["religion"] = html_encode(L.fields["religion"]) - nanoui_data["exploit"]["fingerprint"] = html_encode(L.fields["fingerprint"]) - if(L.fields["antagvis"] == ANTAG_KNOWN || (faction == L.fields["antagfac"] && (L.fields["antagvis"] == ANTAG_SHARED))) - nanoui_data["exploit"]["antagfaction"] = html_encode(L.fields["antagfac"]) - else - nanoui_data["exploit"]["antagfaction"] = html_encode("None") - nanoui_data["exploit_exists"] = 1 - break - -// I placed this here because of how relevant it is. -// You place this in your uplinkable item to check if an uplink is active or not. -// If it is, it will display the uplink menu and return 1, else it'll return false. -// If it returns true, I recommend closing the item's normal menu with "user << browse(null, "window=name")" -/obj/item/proc/active_uplink_check(mob/user as mob) - // Activates the uplink if it's active - if(src.hidden_uplink) - if(src.hidden_uplink.active) - src.hidden_uplink.trigger(user) - return 1 - return 0 + switch(action) + if("buy") + var/datum/uplink_item/UI = (locate(params["ref"]) in uplink.items) + UI.buy(src, usr) + return TRUE + if("lock") + toggle() + SStgui.close_uis(src) + if("select") + selected_cat = params["category"] + return TRUE + if("compact_toggle") + compact_mode = !compact_mode + return TRUE + if("view_exploits") + exploit_id = params["id"] + return TRUE // PRESET UPLINKS // A collection of preset uplinks. // // Includes normal radio uplink, multitool uplink, // implant uplink (not the implant tool) and a preset headset uplink. - /obj/item/device/radio/uplink/New(atom/loc, datum/mind/target_mind, telecrystals) ..(loc) hidden_uplink = new(src, target_mind, telecrystals) diff --git a/nano/templates/uplink.tmpl b/nano/templates/uplink.tmpl deleted file mode 100644 index 0773883d70..0000000000 --- a/nano/templates/uplink.tmpl +++ /dev/null @@ -1,177 +0,0 @@ - - -{{:helper.syndicateMode()}} - - - -

{{:data.welcome}}

-
-
-
- Functions: -
-
-
- {{:helper.link('Request Items', 'gear', {'menu' : 0}, null, 'fixedLeftWider')}} - {{:helper.link('Exploitable Information', 'gear', {'menu' : 2}, null, 'fixedLeftWider')}} -
-
- {{:helper.link('Return', 'arrowreturn-1-w', {'return' : 1}, null, 'fixedLeft')}} - {{:helper.link('Close', 'gear', {'lock' : "1"}, null, 'fixedLeft')}} -
-
-
-
- -
-
- Tele-Crystals: -
-
- {{:data.crystals}} -
-
- - -{{if data.menu == 0}} - {{if data.discount_amount < 100}} -
-
- Currently discounted: -
-
- {{:data.discount_name}} - {{:data.discount_amount}}% off. Offer will expire at: {{:data.offer_expiry}} -
-
- {{/if}} - -
-
Categories
- {{for data.categories}} -
- {{:helper.link(value.name, 'gear', {'category' : value.ref}, (value.name == data.current_category) ? 'categoryActive' : '', 'category')}} -
- {{/for}} -
- -
-
- Request Items -
-
-
-
- {{for data.items}} -
-
- -
{{:value.cost}} points
-
-
- {{/for}} -
-
-
-
- -{{else data.menu == 2}} -

Information Record List:

-
-
- Select a Record -
-
- {{for data.exploit_records}} -
- {{:helper.link(value.Name, 'gear', {'menu' : 21, 'id' : value.id}, null, null)}} -
- {{/for}} -{{else data.menu == 21}} -

Information Record:

-
-
-
-
- {{if data.exploit_exists == 1}} - Name: {{:data.exploit.name}}
- Sex: {{:data.exploit.sex}}
- Species: {{:data.exploit.species}}
- Age: {{:data.exploit.age}}
- Rank: {{:data.exploit.rank}}
- Home System: {{:data.exploit.home_system}}
- Citizenship: {{:data.exploit.citizenship}}
- Faction: {{:data.exploit.faction}}
- Religion: {{:data.exploit.religion}}
- Fingerprint: {{:data.exploit.fingerprint}}
- Other Affiliation: {{:data.exploit.antagfaction}}
- -
Acquired Information:
- Notes:
{{:data.exploit.nanoui_exploit_record}}

- {{else}} - - No exploitative information acquired! -
-
-
- {{/if}} -
-
-
-{{/if}} diff --git a/tgui/packages/tgui/interfaces/Uplink.js b/tgui/packages/tgui/interfaces/Uplink.js new file mode 100644 index 0000000000..36aca2c347 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Uplink.js @@ -0,0 +1,322 @@ +import { createSearch, decodeHtmlEntities } from 'common/string'; +import { Fragment } from 'inferno'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Flex, Input, Section, Table, Tabs, NoticeBox, LabeledList } from '../components'; +import { formatMoney } from '../format'; +import { Window } from '../layouts'; + +const MAX_SEARCH_RESULTS = 25; + +export const Uplink = (props, context) => { + const { data } = useBackend(context); + + const [screen, setScreen] = useLocalState(context, 'screen', 0); + + const { telecrystals } = data; + return ( + + + + {screen === 0 && ( + + ) || screen === 1 && ( + + ) || ( +
+ Error +
+ )} +
+
+ ); +}; + +const UplinkHeader = (props, context) => { + const { act, data } = useBackend(context); + + const { + screen, + setScreen, + } = props; + + const { + discount_name, + discount_amount, + offer_expiry, + } = data; + + return ( +
+ + setScreen(0)}> + Request Items + + setScreen(1)}> + Exploitable Information + + +
+ {discount_amount < 100 && ( + + {discount_name} - {discount_amount}% off. Offer expires at: {offer_expiry} + + ) || ( + + No items currently discounted. + + )} +
+
+ ); +}; + +const ExploitableInformation = (props, context) => { + const { act, data } = useBackend(context); + + const { + exploit, + locked_records, + } = data; + + return ( +
act("view_exploits", { id: 0 })} /> + )}> + {exploit && ( + + + + {exploit.name} + + + {exploit.sex} + + + {exploit.species} + + + {exploit.age} + + + {exploit.rank} + + + {exploit.home_system} + + + {exploit.citizenship} + + + {exploit.faction} + + + {exploit.religion} + + + {exploit.fingerprint} + + + {exploit.antagfaction} + + + Acquired Information + + {exploit.nanoui_exploit_record} + + + + ) || locked_records.map(record => ( +
+ ); +}; + +export const GenericUplink = (props, context) => { + const { + currencyAmount = 0, + currencySymbol = '₮', + } = props; + const { act, data } = useBackend(context); + const { + compactMode, + lockable, + categories = [], + } = data; + const [ + searchText, + setSearchText, + ] = useLocalState(context, 'searchText', ''); + const [ + selectedCategory, + setSelectedCategory, + ] = useLocalState(context, 'category', categories[0]?.name); + const testSearch = createSearch(searchText, item => { + return item.name + item.desc; + }); + const items = searchText.length > 0 + // Flatten all categories and apply search to it + && categories + .flatMap(category => category.items || []) + .filter(testSearch) + .filter((item, i) => i < MAX_SEARCH_RESULTS) + // Select a category and show all items in it + || categories + .find(category => category.name === selectedCategory) + ?.items + // If none of that results in a list, return an empty list + || []; + return ( +
0 ? 'good' : 'bad'}> + {formatMoney(currencyAmount)} {currencySymbol} + + )} + buttons={( + + Search + setSearchText(value)} + mx={1} /> +
+ ); +}; + +const ItemList = (props, context) => { + const { + compactMode, + currencyAmount, + currencySymbol, + } = props; + const { act } = useBackend(context); + const [ + hoveredItem, + setHoveredItem, + ] = useLocalState(context, 'hoveredItem', {}); + const hoveredCost = hoveredItem && hoveredItem.cost || 0; + // Append extra hover data to items + const items = props.items.map(item => { + const notSameItem = hoveredItem && hoveredItem.name !== item.name; + const notEnoughHovered = currencyAmount - hoveredCost < item.cost; + const disabledDueToHovered = notSameItem && notEnoughHovered; + const disabled = currencyAmount < item.cost || disabledDueToHovered; + return { + ...item, + disabled, + }; + }); + if (compactMode) { + return ( + + {items.map(item => ( + + + {decodeHtmlEntities(item.name)} + + +
+ ); + } + return items.map(item => ( +
setHoveredItem(item)} + onmouseout={() => setHoveredItem({})} + onClick={() => act('buy', { + ref: item.ref, + })} /> + )}> + {decodeHtmlEntities(item.desc)} +
+ )); +};