diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm index c2fb07f2510..4b2ac79d8fd 100644 --- a/code/__DEFINES/rust_g.dm +++ b/code/__DEFINES/rust_g.dm @@ -41,6 +41,49 @@ /// Gets the version of rust_g /proc/rustg_get_version() return call(RUST_G, "get_version")() + +/** + * Sets up the Aho-Corasick automaton with its default options. + * + * The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call + * Arguments: + * * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls + * * patterns - A non-associative list of strings to search for + * * replacements - Default replacements for this automaton, used with rustg_acreplace + */ +#define rustg_setup_acreplace(key, patterns, replacements) call(RUST_G, "setup_acreplace")(key, json_encode(patterns), json_encode(replacements)) + +/** + * Sets up the Aho-Corasick automaton using supplied options. + * + * The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call + * Arguments: + * * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls + * * options - An associative list like list("anchored" = 0, "ascii_case_insensitive" = 0, "match_kind" = "Standard"). The values shown on the example are the defaults, and default values may be omitted. See the identically named methods at https://docs.rs/aho-corasick/latest/aho_corasick/struct.AhoCorasickBuilder.html to see what the options do. + * * patterns - A non-associative list of strings to search for + * * replacements - Default replacements for this automaton, used with rustg_acreplace + */ +#define rustg_setup_acreplace_with_options(key, options, patterns, replacements) call(RUST_G, "setup_acreplace")(key, json_encode(options), json_encode(patterns), json_encode(replacements)) + +/** + * Run the specified replacement engine with the provided haystack text to replace, returning replaced text. + * + * Arguments: + * * key - The key for the automaton + * * text - Text to run replacements on + */ +#define rustg_acreplace(key, text) call(RUST_G, "acreplace")(key, text) + +/** + * Run the specified replacement engine with the provided haystack text to replace, returning replaced text. + * + * Arguments: + * * key - The key for the automaton + * * text - Text to run replacements on + * * replacements - Replacements for this call. Must be the same length as the set-up patterns + */ +#define rustg_acreplace_with_replacements(key, text, replacements) call(RUST_G, "acreplace_with_replacements")(key, text, json_encode(replacements)) + /** * This proc generates a cellular automata noise grid which can be used in procedural generation methods. * @@ -65,6 +108,8 @@ #define rustg_file_exists(fname) call(RUST_G, "file_exists")(fname) #define rustg_file_write(text, fname) call(RUST_G, "file_write")(text, fname) #define rustg_file_append(text, fname) call(RUST_G, "file_append")(text, fname) +#define rustg_file_get_line_count(fname) text2num(call(RUST_G, "file_get_line_count")(fname)) +#define rustg_file_seek_line(fname, line) call(RUST_G, "file_seek_line")(fname, "[line]") #ifdef RUSTG_OVERRIDE_BUILTINS #define file2text(fname) rustg_file_read("[fname]") @@ -102,6 +147,10 @@ #define rustg_sql_disconnect_pool(handle) call(RUST_G, "sql_disconnect_pool")(handle) #define rustg_sql_check_query(job_id) call(RUST_G, "sql_check_query")("[job_id]") +#define rustg_time_microseconds(id) text2num(call(RUST_G, "time_microseconds")(id)) +#define rustg_time_milliseconds(id) text2num(call(RUST_G, "time_milliseconds")(id)) +#define rustg_time_reset(id) call(RUST_G, "time_reset")(id) + #define rustg_read_toml_file(path) json_decode(call(RUST_G, "toml_file_to_json")(path) || "null") #define rustg_url_encode(text) call(RUST_G, "url_encode")("[text]") diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index 24e2d863c25..a109e2b8ec9 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -993,7 +993,7 @@ GLOBAL_LIST_INIT(binary, list("0","1")) try return json_decode(data) catch - return + return null /proc/num2loadingbar(percent as num, numSquares = 20, reverse = FALSE) var/loadstring = "" diff --git a/code/datums/components/puzzgrid.dm b/code/datums/components/puzzgrid.dm new file mode 100644 index 00000000000..65ac1a00881 --- /dev/null +++ b/code/datums/components/puzzgrid.dm @@ -0,0 +1,306 @@ +#define PUZZGRID_CONFIG "[global.config.directory]/puzzgrids.txt" +#define PUZZGRID_GROUP_COUNT 4 +#define PUZZGRID_MAX_ATTEMPTS 10 + +/// Attaches a puzzgrid to the atom. +/// You are expected to pass in the puzzgrid, likely from create_random_puzzgrid(). +/// This is so you can handle when a puzzgrid can't be generated, either because the +/// config does not exist, or because the config is not set up properly. +/datum/component/puzzgrid + var/datum/puzzgrid/puzzgrid + + /// Callback that will be called when you win + var/datum/callback/on_victory_callback + + /// Callback that will be called when you lose, either through running out of time or running out of lives + var/datum/callback/on_fail_callback + + /// The world timestamp for when the puzzgrid will fail, if timer was set in Initialize + var/time_to_finish + + /// Every answer, in text, including already solved ones + var/list/all_answers + + /// The answers, in text, that are currently selected + var/list/selected_answers = list() + + /// The puzzgrid groups that have already been solved + var/list/datum/puzzgrid_group/solved_groups = list() + + /// The number of lives left + var/lives = 3 + + COOLDOWN_DECLARE(wrong_group_select_cooldown) + +/datum/component/puzzgrid/Initialize( + datum/puzzgrid/puzzgrid, + timer, + datum/callback/on_victory_callback, + datum/callback/on_fail_callback, +) + if (!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + if (!istype(puzzgrid)) + stack_trace("Invalid puzzgrid passed: [puzzgrid]") + return COMPONENT_INCOMPATIBLE + + src.puzzgrid = puzzgrid + src.on_victory_callback = on_victory_callback + src.on_fail_callback = on_fail_callback + + all_answers = puzzgrid.answers.Copy() + + if (!isnull(timer)) + addtimer(CALLBACK(src, .proc/out_of_time), timer) + time_to_finish = world.time + timer + +/datum/component/puzzgrid/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand) + +/datum/component/puzzgrid/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND) + +/datum/component/puzzgrid/proc/on_attack_hand(atom/source, mob/user) + SIGNAL_HANDLER + + INVOKE_ASYNC(src, .proc/ui_interact, user) + +/datum/component/puzzgrid/ui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Puzzgrid") + ui.open() + +/datum/component/puzzgrid/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return . + + switch (action) + if ("select") + return try_select(params["answer"]) + if ("unselect") + return try_unselect(params["answer"]) + + return TRUE + +/datum/component/puzzgrid/proc/try_select(answer) + if (!(answer in all_answers)) + return FALSE + + if (!COOLDOWN_FINISHED(src, wrong_group_select_cooldown)) + return TRUE + + selected_answers |= answer + + if (selected_answers.len < PUZZGRID_GROUP_COUNT) + return TRUE + + var/list/current_selected_answers = selected_answers + selected_answers = list() + + search_group: + for (var/datum/puzzgrid_group/puzzgrid_group in (puzzgrid.groups - solved_groups)) + for (var/selected_answer in current_selected_answers) + if (!(selected_answer in puzzgrid_group.answers)) + continue search_group + + // This group has the right answers + solved_groups += puzzgrid_group + + if (solved_groups.len == puzzgrid.groups.len - 1) + on_victory() + else + update_static_data_for_all_viewers() + + return TRUE + + COOLDOWN_START(src, wrong_group_select_cooldown, 0.2 SECONDS) + + if (solved_groups.len == puzzgrid.groups.len - 2) + lives -= 1 + + if (lives == 0) + out_of_lives() + + return TRUE + +/datum/component/puzzgrid/proc/try_unselect(answer) + selected_answers -= answer + return TRUE + +/datum/component/puzzgrid/proc/on_victory() + report_answers() + on_victory_callback?.InvokeAsync() + qdel(src) + +/datum/component/puzzgrid/proc/out_of_lives() + var/atom/movable/movable_parent = parent + if (istype(movable_parent)) + movable_parent.say("Ran out of lives!", forced = "puzzgrid component") + + fail() + +/datum/component/puzzgrid/proc/out_of_time() + var/atom/movable/movable_parent = parent + if (istype(movable_parent)) + movable_parent.say("Ran out of time!", forced = "puzzgrid component") + + fail() + +/datum/component/puzzgrid/proc/fail() + report_answers() + on_fail_callback?.InvokeAsync() + qdel(src) + +/datum/component/puzzgrid/proc/report_answers() + var/list/answers = list() + for (var/datum/puzzgrid_group/puzzgrid_group as anything in puzzgrid.groups) + var/list/answers_encoded = list() + for (var/answer in puzzgrid_group.answers) + answers_encoded += html_encode(answer) + + answers += span_boldnotice("

[answers_encoded.Join(", ")]

") + span_notice("

[html_encode(puzzgrid_group.description)]

") + + var/message = answers.Join("

-----

") + + for (var/mob/mob as anything in get_hearers_in_view(DEFAULT_MESSAGE_RANGE, src)) + to_chat(mob, message) + +/datum/component/puzzgrid/ui_data(mob/user) + return list( + "selected_answers" = selected_answers, + "time_left" = time_to_finish && (max(0, (time_to_finish - world.time) / (1 SECONDS))), + "wrong_group_select_cooldown" = !COOLDOWN_FINISHED(src, wrong_group_select_cooldown), + "lives" = lives, + ) + +/datum/component/puzzgrid/ui_static_data(mob/user) + var/list/data = list() + + data["answers"] = puzzgrid.answers + + var/list/serialized_solved_groups = list() + for (var/datum/puzzgrid_group/solved_group as anything in solved_groups) + serialized_solved_groups += list(list( + "answers" = solved_group.answers, + )) + + var/atom/atom_parent = parent + + data["host"] = atom_parent.name + data["solved_groups"] = serialized_solved_groups + + return data + +/// Returns a random puzzgrid from config. +/// If config is empty, or no valid puzzgrids can be found in time, will return null. +/proc/create_random_puzzgrid() + var/static/total_lines + + if (isnull(total_lines)) + total_lines = rustg_file_get_line_count(PUZZGRID_CONFIG) + + if (isnull(total_lines)) + // There was an error reading the file + total_lines = 0 + + if (total_lines == 0) + return null + + for (var/_ in 1 to PUZZGRID_MAX_ATTEMPTS) + var/line_number = rand(0, total_lines - 1) + var/line = rustg_file_seek_line(PUZZGRID_CONFIG, line_number) + if (!line) + continue + + var/line_json_decoded = safe_json_decode(line) + if (isnull(line_json_decoded)) + log_config("Line [line_number + 1] in puzzgrids.txt is not a JSON: [line]") + continue + + var/datum/puzzgrid/puzzgrid = new + var/populate_result = puzzgrid.populate(line_json_decoded) + + if (populate_result == TRUE) + return puzzgrid + else + log_config("Line [line_number + 1] in puzzgrids.txt is not formatted correctly: [populate_result]") + + stack_trace("No valid puzzgrid config could be found in [PUZZGRID_MAX_ATTEMPTS] attempts, please check config_error. If it is empty, then seek line is failing.") + return null + +/// Represents an individual puzzgrid +/datum/puzzgrid + var/list/answers = list() + var/list/datum/puzzgrid_group/groups = list() + +/// Will populate a puzzgrid with the information from the JSON. +/// Will return TRUE if the populate succeeded, or a string denoting the error otherwise. +/datum/puzzgrid/proc/populate(list/from_json) + if (!islist(from_json)) + return "Puzzgrid was not a list" + + var/list/answers = list() + var/list/groups = list() + + for (var/group_json in from_json) + if (!islist(group_json)) + return "Group was not a list (received [json_encode(group_json)])" + + if (!("cells" in group_json)) + return "Group did not have a 'cells' field (received [json_encode(group_json)])" + + if (!("description" in group_json)) + return "Group did not have a 'description' field (received [json_encode(group_json)])" + + var/datum/puzzgrid_group/group = new + group.answers = group_json["cells"] + group.description = group_json["description"] + + answers += group.answers + + groups += group + + src.answers = shuffle(answers) + src.groups = groups + + return TRUE + +/// Represents an individual group in a puzzgrid +/datum/puzzgrid_group + var/list/answers = list() + var/description + +/// Debug verb for validating that all puzzgrids can be created successfully. +/// Locked behind a verb because it's fairly slow and memory intensive. +/client/proc/validate_puzzgrids() + set name = "Validate Puzzgrid Config" + set category = "Debug" + + var/line_number = 0 + + for (var/line in world.file2list(PUZZGRID_CONFIG)) + line_number += 1 + + if (length(line) == 0) + continue + + var/line_json_decoded = safe_json_decode(line) + if (isnull(line_json_decoded)) + to_chat(src, span_warning("Line [line_number] in puzzgrids.txt is not a JSON: [line]")) + continue + + var/datum/puzzgrid/puzzgrid = new + var/populate_result = puzzgrid.populate(line_json_decoded) + + if (populate_result != TRUE) + to_chat(src, span_warning("Line [line_number] in puzzgrids.txt is not formatted correctly: [populate_result]")) + + to_chat(src, span_notice("Validated. If you did not see any errors, you're in the clear.")) + +#undef PUZZGRID_CONFIG +#undef PUZZGRID_GROUP_COUNT +#undef PUZZGRID_MAX_ATTEMPTS diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 0edf9951aa8..f424c20d432 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -218,6 +218,7 @@ GLOBAL_PROTECT(admin_verbs_debug) /client/proc/cmd_admin_toggle_fov, /client/proc/cmd_admin_debug_traitor_objectives, /client/proc/spawn_debug_full_crew, + /client/proc/validate_puzzgrids, ) GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release)) GLOBAL_PROTECT(admin_verbs_possess) diff --git a/code/modules/admin/smites/puzzgrid.dm b/code/modules/admin/smites/puzzgrid.dm new file mode 100644 index 00000000000..80b7f795ba9 --- /dev/null +++ b/code/modules/admin/smites/puzzgrid.dm @@ -0,0 +1,118 @@ +/// Turns the user into a puzzgrid +/datum/smite/puzzgrid + name = "Puzzgrid" + + var/timer + var/gib_on_loss + +/datum/smite/puzzgrid/configure(client/user) + var/timer = input(user, "How long should other people have to solve the grid? 0 gives infinite time.", "Puzzgrid", 0) as num | null + if (isnull(timer)) + return FALSE + + var/gib_on_loss = tgui_alert(user, "What should happen to them when they lose?", "Puzzgrid", list("Gib", "New puzzle")) == "Gib" + + src.gib_on_loss = gib_on_loss + src.timer = timer == 0 ? null : (timer * 1 SECONDS) + + return TRUE + +/datum/smite/puzzgrid/effect(client/user, mob/living/target) + . = ..() + + var/datum/puzzgrid/puzzgrid = create_random_puzzgrid() + if (isnull(puzzgrid)) + to_chat(user, span_warning("Couldn't create a puzzgrid! Maybe the config isn't setup?")) + return + + var/obj/structure/puzzgrid_effect/puzzgrid_effect = new(target.loc, target, puzzgrid, timer, gib_on_loss) + target.forceMove(puzzgrid_effect) + puzzgrid_effect.visible_message(span_warning("[target] has suddenly transformed into a fiendishly hard puzzle!")) + + playsound(puzzgrid_effect, 'sound/effects/magic.ogg', 70) + +/obj/structure/puzzgrid_effect + anchored = TRUE + density = TRUE + resistance_flags = INDESTRUCTIBLE + icon = 'icons/effects/effects.dmi' + icon_state = "shield2" + + var/mob/living/victim + var/timer + var/gib_on_loss + +/obj/structure/puzzgrid_effect/Initialize(mapload, mob/living/victim, datum/puzzgrid/puzzgrid, timer, gib_on_loss) + . = ..() + + if (isnull(victim)) + return + + src.victim = victim + src.timer = timer + src.gib_on_loss = gib_on_loss + + name = "[victim]'s fiendish curse" + + ADD_TRAIT(victim, TRAIT_HANDS_BLOCKED, "[type]") + ADD_TRAIT(victim, TRAIT_IMMOBILIZED, "[type]") + + add_puzzgrid_component(puzzgrid) + +/obj/structure/puzzgrid_effect/Destroy() + QDEL_NULL(victim) + return ..() + +/obj/structure/puzzgrid_effect/proc/add_puzzgrid_component(datum/puzzgrid/puzzgrid) + AddComponent( \ + /datum/component/puzzgrid, \ + puzzgrid = puzzgrid, \ + timer = timer, \ + on_victory_callback = CALLBACK(src, .proc/on_victory), \ + on_fail_callback = CALLBACK(src, gib_on_loss ? .proc/loss_gib : .proc/loss_restart), \ + ) + +/obj/structure/puzzgrid_effect/proc/on_victory() + victim.forceMove(loc) + victim.Paralyze(5 SECONDS) + victim.visible_message( + span_notice("[victim] is unshackled from their fiendish prison!"), + span_notice("You are unshackled from your fiendish prison!"), + ) + + remove_traits() + + victim = null + + qdel(src) + +/obj/structure/puzzgrid_effect/proc/loss_gib() + victim.forceMove(loc) + victim.visible_message( + span_bolddanger("You were unable to free [victim] from their fiendish prison, leaving them as nothing more than a smattering of mush!"), + span_bolddanger("Your compatriates were unable to free you from your fiendish prison, leaving you as nothing more than a smattering of mush!"), + ) + victim.gib() + victim = null + + qdel(src) + +/obj/structure/puzzgrid_effect/proc/loss_restart() + var/datum/puzzgrid/puzzgrid = create_random_puzzgrid() + if (isnull(puzzgrid)) + victim.forceMove(loc) + victim.Paralyze(5 SECONDS) + victim.visible_message(span_bolddanger("Despite completely failing the puzzle, through unbelievable luck, [victim] manages to break out anyway!")) + remove_traits() + qdel(src) + victim = null + return + + visible_message(span_danger("The fiendishly hard puzzle shapeshifts into a different, equally as challenging puzzle!")) + + // Defer until after the fail proc finishes, since that will qdel the component. + addtimer(CALLBACK(src, .proc/add_puzzgrid_component, puzzgrid), 0) + +/obj/structure/puzzgrid_effect/proc/remove_traits() + REMOVE_TRAIT(victim, TRAIT_HANDS_BLOCKED, "[type]") + REMOVE_TRAIT(victim, TRAIT_IMMOBILIZED, "[type]") diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index c37d5ff6c45..719aa475db7 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -493,7 +493,7 @@ GLOBAL_VAR_INIT(say_disabled, FALSE) var/not_told = 0 var/list/valid_device_types = typecacheof(list( - /obj/machinery/computer/atmos_control, + /obj/machinery/computer/atmos_control, /obj/machinery/air_sensor, /obj/machinery/atmospherics/components/unary/outlet_injector/monitored, /obj/machinery/meter/monitored, diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 69888bd6417..6af2f94d2cb 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -62,6 +62,17 @@ if(ui) ui.send_full_update() +/** + * public + * + * Will force an update on static data for all viewers. + * Should be done manually whenever something happens to + * change static data. + */ +/datum/proc/update_static_data_for_all_viewers() + for (var/datum/tgui/window as anything in SStgui.open_uis_by_src[REF(src)]) + window.send_full_update() + /** * public * diff --git a/config/puzzgrids.txt b/config/puzzgrids.txt new file mode 100644 index 00000000000..56e3ee99702 --- /dev/null +++ b/config/puzzgrids.txt @@ -0,0 +1,4 @@ +[{"cells": ["Hour", "Year", "Second", "Day"], "description": "Measurements of time."}, {"cells": ["Items", "Emits", "Smite", "Mites"], "description": "Anagrams of each other."}, {"cells": ["Little", "Miniature", "Tiny", "Wee"], "description": "Synonyms of small."}, {"cells": ["City", "Minute", "Times", "Mets"], "description": "Words that can follow \"New York.\""}] +[{"cells": ["Ben", "Rob", "Dave", "Nick"], "description": "Shortened Boys Names"}, {"cells": ["Scrambled", "Boiled", "Fried", "Easter"], "description": "Eggs"}, {"cells": ["Poached", "Took", "Stole", "Plundered"], "description": "Words for theft"}, {"cells": ["Beech", "Willow", "Oak", "Christmas"], "description": "______Trees"}] +[{"cells": ["Earth", "Book", "Silk", "Inch"], "description": "Types of worm."}, {"cells": ["Humpback", "Blue", "Beluga", "Killer"], "description": "Types of whale."}, {"cells": ["Cerulean", "Azure", "Cornflower", "Cobalt"], "description": "Shades of blue."}, {"cells": ["Obtuse", "Acute", "Right", "Straight"], "description": "Types of angle"}] +[{"cells": ["Celsius", "Carbon; Periodically.", "Coding Language", "Cosmic Constant"], "description": "All are represented as \"C\"\n\nCarbon is \"C\" on the periodic table.\nCosmic Constant is the speed of light, represented as \"c\" (E = mc^2 for example)\nCoding Language is \"C\"\nCelsius is represented as \"C\""}, {"cells": ["Catch A Glimpse", "Cones & Rods Function", "Coupled With Saw", "Catholic Governing Body"], "description": "All items relate to the word \"see\"\n\nCones & Rods in your eyes help you to \"see\"\nCatholic Governing Body is the Holy \"See\"\n\"See\" Saw\nCatch A Glimpse is to \"See\""}, {"cells": ["China (South)", "Could Be Black", "Could Be Red", "Caspian"], "description": "All items refer to \"seas\"\n\nCaspian Sea\nRed Sea\nBlack Sea\nSouth China Sea"}, {"cells": ["Coupled With Fire", "Cancel", "Companion To Desist", "Come To An End"], "description": "All are associated with the word \"cease\"\n\nCease and desist.\nTo cease is to come to an end.\nCeasefire.\nTo cancel something is to cease its operation."}] diff --git a/rust_g.dll b/rust_g.dll index be19f57946d..b72315ec5cf 100644 Binary files a/rust_g.dll and b/rust_g.dll differ diff --git a/tgstation.dme b/tgstation.dme index 2e0e3dd7e02..afc3eb5daec 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -844,6 +844,7 @@ #include "code\datums\components\phylactery.dm" #include "code\datums\components\pricetag.dm" #include "code\datums\components\punchcooldown.dm" +#include "code\datums\components\puzzgrid.dm" #include "code\datums\components\radiation_countdown.dm" #include "code\datums\components\reagent_refiller.dm" #include "code\datums\components\religious_tool.dm" @@ -2002,6 +2003,7 @@ #include "code\modules\admin\smites\lightning.dm" #include "code\modules\admin\smites\nugget.dm" #include "code\modules\admin\smites\phobia_ocky_icky.dm" +#include "code\modules\admin\smites\puzzgrid.dm" #include "code\modules\admin\smites\puzzle.dm" #include "code\modules\admin\smites\rod.dm" #include "code\modules\admin\smites\scarify.dm" diff --git a/tgui/packages/tgui/interfaces/Puzzgrid.tsx b/tgui/packages/tgui/interfaces/Puzzgrid.tsx new file mode 100644 index 00000000000..85251d5098b --- /dev/null +++ b/tgui/packages/tgui/interfaces/Puzzgrid.tsx @@ -0,0 +1,139 @@ +import { range } from "common/collections"; +import { BooleanLike } from "common/react"; +import { SFC } from "inferno"; +import { useBackend } from "../backend"; +import { Box, Button, FitText, Stack } from "../components"; +import { Window } from "../layouts"; + +const CELLS_PER_GROUP = 4; +const CELL_WIDTH = 150; +const CELL_HEIGHT = 100; + +type PuzzgridGroup = { + answers: string[] +}; + +type PuzzgridData = { + answers: string[], + host: string, + lives: number, + selected_answers: string[], + solved_groups: PuzzgridGroup[], + time_left: number, + wrong_group_select_cooldown: BooleanLike, +}; + +const PuzzgridButton: SFC<{ + // In the future, this would be the TypeScript props of the button + [key: string]: unknown, +}> = (props) => { + return ( + + ); +}; + +export const Puzzgrid = (props, context) => { + const { act, data } = useBackend(context); + + const answersLeft = data.answers.filter(answer => ( + !data.solved_groups.find(group => group.answers.indexOf(answer) !== -1) + )); + + return ( + + + + {data.solved_groups.map((group, groupIndex) => ( + + + {group.answers.map((answer, answerIndex) => { + return ( + + + {answer} + + + ); + })} + + + ))} + + {range(0, answersLeft.length / CELLS_PER_GROUP).map(row => ( + + + {range(0, CELLS_PER_GROUP).map(column => { + const answer = answersLeft[(row * CELLS_PER_GROUP) + column]; + const selected = data.selected_answers.indexOf(answer) !== -1; + + return ( + + act(selected ? "unselect" : "select", { + answer, + })} + > + {answer} + + + ); + })} + + + ))} + + + {(data.solved_groups.length === CELLS_PER_GROUP - 2) && ( + + {range(0, data.lives).map(live => ( + + ♥ + + ))} + + )} + + {data.time_left && ( + + {Math.ceil(data.time_left)}s + + )} + + + ); +};