diff --git a/_maps/tutorials/tutorial_12x12.dmm b/_maps/tutorials/tutorial_12x12.dmm new file mode 100644 index 00000000000..b3259ac75aa --- /dev/null +++ b/_maps/tutorials/tutorial_12x12.dmm @@ -0,0 +1,184 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/closed/wall, +/area/tutorial) +"o" = ( +/turf/open/floor/plasteel, +/area/tutorial) +"H" = ( +/obj/effect/light_emitter, +/turf/open/floor/plasteel, +/area/tutorial) +"I" = ( +/obj/effect/landmark/tutorial_bottom_left, +/turf/open/floor/plasteel, +/area/tutorial) + +(1,1,1) = {" +a +a +a +a +a +a +a +a +a +a +a +a +"} +(2,1,1) = {" +a +o +o +o +o +o +o +o +o +o +I +a +"} +(3,1,1) = {" +a +o +H +o +o +o +o +o +o +H +o +a +"} +(4,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(5,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(6,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(7,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(8,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(9,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(10,1,1) = {" +a +o +H +o +o +o +o +o +o +H +o +a +"} +(11,1,1) = {" +a +o +o +o +o +o +o +o +o +o +o +a +"} +(12,1,1) = {" +a +a +a +a +a +a +a +a +a +a +a +a +"} diff --git a/_maps/tutorials/tutorial_7x7.dmm b/_maps/tutorials/tutorial_7x7.dmm new file mode 100644 index 00000000000..6e78326ba4d --- /dev/null +++ b/_maps/tutorials/tutorial_7x7.dmm @@ -0,0 +1,79 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/closed/wall, +/area/tutorial) +"G" = ( +/turf/open/floor/plasteel, +/area/tutorial) +"Q" = ( +/obj/effect/landmark/tutorial_bottom_left, +/turf/open/floor/plasteel, +/area/tutorial) +"S" = ( +/obj/effect/light_emitter, +/turf/open/floor/plasteel, +/area/tutorial) + +(1,1,1) = {" +a +a +a +a +a +a +a +"} +(2,1,1) = {" +a +G +G +G +G +Q +a +"} +(3,1,1) = {" +a +G +G +G +G +G +a +"} +(4,1,1) = {" +a +G +G +S +G +G +a +"} +(5,1,1) = {" +a +G +G +G +G +G +a +"} +(6,1,1) = {" +a +G +G +G +G +G +a +"} +(7,1,1) = {" +a +a +a +a +a +a +a +"} diff --git a/aquila/aquila.dm b/aquila/aquila.dm index 46c7ae64571..ddcaff7d5b4 100644 --- a/aquila/aquila.dm +++ b/aquila/aquila.dm @@ -14,23 +14,28 @@ #include "code\__DEFINES\sound.dm" #include "code\__DEFINES\span.dm" #include "code\__DEFINES\traits.dm" +#include "code\__DEFINES\tutorial.dm" #include "code\__DEFINES\uplink.dm" #include "code\__DEFINES\wires.dm" #include "code\__DEFINES\_onclick\huds\vampire.dm" +#include "code\__HELPERS\areas.dm" #include "code\__HELPERS\names.dm" #include "code\__HELPERS\game.dm" +#include "code\__HELPERS\text.dm" #include "code\__HELPERS\unsorted.dm" #include "code\_globalvars\lists\game.dm" #include "code\__HELPERS\pronouns.dm" #include "code\_onclick\hud\alert.dm" #include "code\controllers\configuration\entries\game_options.dm" #include "code\controllers\configuration\entries\general.dm" +#include "code\controllers\subsystem\async_map_generator.dm" #include "code\controllers\subsystem\demo.dm" #include "code\controllers\subsystem\jukeboxes.dm" #include "code\controllers\subsystem\vote.dm" #include "code\datum\wires\wires_jukebox.dm" #include "code\datums\components\nanites.dm" #include "code\datums\components\uplink.dm" +#include "code\datums\components\tutorial_status.dm" #include "code\datums\diseases\advance\symptoms\fleshgrowth.dm" #include "code\datums\diseases\transformation.dm" #include "code\datums\action.dm" @@ -51,6 +56,14 @@ #include "code\datums\weather\shitstorm.dm" #include "code\datums\saymode.dm" #include "code\datums\mind.dm" +#include "code\datums\map_generators\_async_map_generator.dm" +#include "code\datums\map_generators\map_placer.dm" +#include "code\datums\tutorial\_tutorial.dm" +#include "code\datums\tutorial\_tutorial_menu.dm" +#include "code\datums\tutorial\ss13\_ss13.dm" +#include "code\datums\tutorial\ss13\basic_ss13.dm" +#include "code\datums\tutorial\ss13\intents_ss13.dm" +#include "code\game\area\Space_Station_13_areas.dm" #include "code\game\area\areas\centcom.dm" #include "code\game\area\areas\shuttles.dm" #include "code\game\atoms_movable.dm" @@ -177,6 +190,8 @@ #include "code\modules\food_and_drinks\drinks\drinks.dm" #include "code\modules\food_and_drinks\food\snacks_pie.dm" #include "code\modules\food_and_drinks\recipes\drinks_recipes.dm" +#include "code\modules\mapping\map_template.dm" +#include "code\modules\mapping\reader.dm" #include "code\modules\metacoin\metacoin.dm" #include "code\modules\mining\equipment\mineral_scanner.dm" #include "code\modules\mining\machine_bluespaceminer.dm" diff --git a/aquila/code/__DEFINES/traits.dm b/aquila/code/__DEFINES/traits.dm index 9b1f4d6b7d2..f62fb8fc39e 100644 --- a/aquila/code/__DEFINES/traits.dm +++ b/aquila/code/__DEFINES/traits.dm @@ -5,7 +5,12 @@ #define TRAIT_EAT_MORE "eat_more" #define TRAIT_BOTTOMLESS_STOMACH "bottomless_stomach" #define TRAIT_GENELESS "geneless" +#define TRAIT_IN_TUTORIAL "in_tutorial" //non-mob traits #define SINFULDEMON_TRAIT "sinfuldemon" + + +/// Trait source from Tutorial +#define TRAIT_SOURCE_TUTORIAL "tutorials" diff --git a/aquila/code/__DEFINES/tutorial.dm b/aquila/code/__DEFINES/tutorial.dm new file mode 100644 index 00000000000..5b6efd9fc27 --- /dev/null +++ b/aquila/code/__DEFINES/tutorial.dm @@ -0,0 +1,2 @@ +#define TUTORIAL_CATEGORY_BASE "Base" // Shouldn't be used outside of base types +#define TUTORIAL_CATEGORY_SS13 "Space Station 13" diff --git a/aquila/code/__HELPERS/areas.dm b/aquila/code/__HELPERS/areas.dm new file mode 100644 index 00000000000..23f37dec19b --- /dev/null +++ b/aquila/code/__HELPERS/areas.dm @@ -0,0 +1,2 @@ +/proc/require_area_resort() + GLOB.sortedAreas = null diff --git a/aquila/code/__HELPERS/text.dm b/aquila/code/__HELPERS/text.dm new file mode 100644 index 00000000000..8b831bfa4fd --- /dev/null +++ b/aquila/code/__HELPERS/text.dm @@ -0,0 +1,17 @@ +/// Returns a string with reserved characters and spaces after the first and last letters removed +/// Like trim(), but very slightly faster. worth it for niche usecases +/proc/trim_reduced(text) + var/starting_coord = 1 + var/text_len = length(text) + for (var/i in 1 to text_len) + if (text2ascii(text, i) > 32) + starting_coord = i + break + + for (var/i = text_len, i >= starting_coord, i--) + if (text2ascii(text, i) > 32) + return copytext(text, starting_coord, i + 1) + + if(starting_coord > 1) + return copytext(text, starting_coord) + return "" diff --git a/aquila/code/controllers/subsystem/async_map_generator.dm b/aquila/code/controllers/subsystem/async_map_generator.dm new file mode 100644 index 00000000000..7db795effaa --- /dev/null +++ b/aquila/code/controllers/subsystem/async_map_generator.dm @@ -0,0 +1,54 @@ +SUBSYSTEM_DEF(async_map_generator) + name = "Async Map Generator" + wait = 1 + flags = SS_TICKER | SS_NO_INIT + runlevels = ALL + + /// List of all currently executing generator datums + var/list/executing_generators = list() + + /// Index of current run + var/current_run_index + + /// Length of current run + var/current_run_length + +/datum/controller/subsystem/async_map_generator/stat_entry() + var/list/things = list() + for(var/datum/async_map_generator/running_generator as() in executing_generators) + things += "{Ticks: [running_generator.ticks]}" + . = ..("GenCnt:[length(executing_generators)], [things.Join(",")]") + +/datum/controller/subsystem/async_map_generator/fire() + if (!length(executing_generators)) + return + //Reset the queue + if (current_run_index > current_run_length || !current_run_length) + current_run_index = 1 + current_run_length = length(executing_generators) + //Split the tick + MC_SPLIT_TICK_INIT(current_run_length) + //Start processing + while (current_run_index <= current_run_length) + //Get current action + var/datum/async_map_generator/currently_running = executing_generators[current_run_index] + current_run_index ++ + //Perform generate action + var/completed = TRUE + while (!currently_running.execute_run()) + // We overused our allocated amount of tick + if(MC_TICK_CHECK) + completed = FALSE + break + //We completed + if (completed) + currently_running.complete() + //Remove the currently running generator + executing_generators -= currently_running + //Decrement the current run nidex + current_run_index -- + //Decrement the current run length + current_run_length -- + to_chat(world, "Fully completed running map generator [current_run_index + 1].") + //Continue to the next process + MC_SPLIT_TICK diff --git a/aquila/code/datums/components/tutorial_status.dm b/aquila/code/datums/components/tutorial_status.dm new file mode 100644 index 00000000000..eaa2d77a1a6 --- /dev/null +++ b/aquila/code/datums/components/tutorial_status.dm @@ -0,0 +1,20 @@ +/datum/component/tutorial_status + dupe_mode = COMPONENT_DUPE_UNIQUE + /// What the mob's current tutorial status is, displayed in the status panel + var/tutorial_status = "" + +/datum/component/tutorial_status/Initialize() + . = ..() + if(!ismob(parent)) + return COMPONENT_INCOMPATIBLE + +/datum/component/tutorial_status/proc/update_objective(datum/source, objective_text) + SIGNAL_HANDLER + + tutorial_status = objective_text + +/datum/component/tutorial_status/proc/get_status_tab_item(datum/source, list/status_tab_items) + SIGNAL_HANDLER + + if(tutorial_status) + status_tab_items += "Tutorial Objective: " + tutorial_status diff --git a/aquila/code/datums/map_generators/_async_map_generator.dm b/aquila/code/datums/map_generators/_async_map_generator.dm new file mode 100644 index 00000000000..02ab27bbf67 --- /dev/null +++ b/aquila/code/datums/map_generators/_async_map_generator.dm @@ -0,0 +1,33 @@ +/// ====================================== +/// Ruin generator process holder +/// ====================================== +/datum/async_map_generator + var/completed = FALSE + var/ticks = 0 + var/list/datum/callback/completion_callbacks = list() + var/list/callback_args + +/// Begin generating +/datum/async_map_generator/proc/generate(...) + callback_args = args.Copy(1) + +/datum/async_map_generator/proc/on_completion(datum/callback/completion_callback) + completion_callbacks += completion_callback + +/// Execute a current run. +/// Returns TRUE if finished +/datum/async_map_generator/proc/execute_run() + ticks ++ + return TRUE + +/datum/async_map_generator/proc/get_name() + return "Async Map generator" + +/datum/async_map_generator/proc/complete() + completed = TRUE + var/list/arguments = list(src) + if (callback_args) + arguments += callback_args + for (var/datum/callback/on_completion as() in completion_callbacks) + on_completion.Invoke(arglist(arguments)) + to_chat(world, "[get_name()] completed and loaded successfully.") diff --git a/aquila/code/datums/map_generators/map_placer.dm b/aquila/code/datums/map_generators/map_placer.dm new file mode 100644 index 00000000000..0b344c820bc --- /dev/null +++ b/aquila/code/datums/map_generators/map_placer.dm @@ -0,0 +1,343 @@ +/////////////////////////////////////////////////////////////// +//SS13 (un)Optimized Map loader +////////////////////////////////////////////////////////////// + +#define GENERATE_STAGE_BUILD_CACHE_START 0 +#define GENERATE_STAGE_BUILD_CACHE 1 +#define GENERATE_STAGE_BUILD_COORDINATES_START 2 +#define GENERATE_STAGE_BUILD_COORDINATES 3 +#define GENERATE_STAGE_COMPLETED 4 + +/datum/async_map_generator/map_place + /// The map template we are placing + var/datum/parsed_map/placing_template + + //========================= + // Generation Parameters + //========================= + + var/x_offset + var/y_offset + var/z_offset + var/crop_map = FALSE + var/no_changeturf = FALSE + var/x_lower = -INFINITY + var/x_upper = INFINITY + var/y_lower = -INFINITY + var/y_upper = INFINITY + var/place_on_top = FALSE + + //========================= + // Generation Run Variables + //========================= + + var/current_run = GENERATE_STAGE_BUILD_CACHE_START + var/run_stage = 0 + + var/list/area_cache = list() + var/list/model_cache + var/space_key = null + var/list/bounds + + //========================= + // Build Cache Locals + //========================= + + var/list/grid_models + + var/grid_model_index + + var/model_key + var/model + var/list/members + var/list/members_attributes + var/index + var/old_position + var/dpos + + //========================= + // Build Coordinate Locals + //========================= + + var/datum/grid_set/gset + + /// The index of the grid line we are currently working on. + /// Start at infinity as we need to access the outer loop first. + var/current_grid_line = INFINITY + + var/ycrd + var/zcrd + var/zexpansion + +/datum/async_map_generator/map_place/New(datum/parsed_map/placing_template, x_offset = 1, y_offset = 1, z_offset = world.maxz + 1, cropMap = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, placeOnTop = FALSE) + . = ..() + src.placing_template = placing_template + src.x_offset = x_offset + src.y_offset = y_offset + src.z_offset = z_offset + crop_map = cropMap + src.no_changeturf = no_changeturf + src.x_lower = x_lower + src.x_upper = x_upper + src.y_lower = y_lower + src.y_upper = y_upper + place_on_top = placeOnTop + +/datum/async_map_generator/map_place/execute_run() + ..() + if (current_run == GENERATE_STAGE_BUILD_CACHE_START) + build_cache_start() + if (current_run == GENERATE_STAGE_BUILD_CACHE) + build_cache() + if (current_run == GENERATE_STAGE_BUILD_COORDINATES_START) + build_coordinates_start() + if (current_run == GENERATE_STAGE_BUILD_COORDINATES) + SSatoms.map_loader_begin(REF(src)) + build_coordinates() + SSatoms.map_loader_stop(REF(src)) + . = current_run == GENERATE_STAGE_COMPLETED + +/datum/async_map_generator/map_place/proc/set_stage(stage) + run_stage = 1 + current_run = stage + +/datum/async_map_generator/map_place/get_name() + return placing_template?.original_path || "Unkown map" + +//====================================== +// COORDINATE BUILDING +//====================================== + +/datum/async_map_generator/map_place/proc/build_coordinates_start() + //Locate the space key + space_key = model_cache[SPACE_KEY] + //Set them all to the same reference, so changing one affects the other + bounds = placing_template.bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + //Move to the next stage + set_stage(GENERATE_STAGE_BUILD_COORDINATES) + +/datum/async_map_generator/map_place/proc/build_coordinates() + while (TRUE) + // Perform inner loop first + while (gset && current_grid_line <= length(gset.gridLines)) + // Build a single grid line + build_coordinate_grid_line() + //Building the grid line overran tick + if (TICK_CHECK) + return + // Enumerate to the next grid set + run_stage ++ + // Check if we are still within bounds + if (run_stage - 1 > length(placing_template.gridSets)) + if(!no_changeturf) + for(var/t in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))) + var/turf/T = t + //we do this after we load everything in. if we don't; we'll have weird atmos bugs regarding atmos adjacent turfs + T.AfterChange(CHANGETURF_IGNORE_AIR) + + // Testing message +#ifdef TESTING + if(placing_template.turfsSkipped) + testing("Skipped loading [placing_template.turfsSkipped] default turfs") +#endif + + // Refresh atmospherics grid + if(placing_template.did_expand) + world.refresh_atmos_grid() + + // Generation completed + set_stage(GENERATE_STAGE_COMPLETED) + return + // Perform grid set action + gset = placing_template.gridSets[run_stage - 1] + ycrd = gset.ycrd + y_offset - 1 + zcrd = gset.zcrd + z_offset - 1 + //Set the current grid line back to 1, for the next iteration + current_grid_line = 1 + if(!crop_map && ycrd > world.maxy) + // Expand Y here. X is expanded in the loop below + world.maxy = ycrd + placing_template.did_expand = TRUE + zexpansion = zcrd > world.maxz + if(zexpansion) + if(crop_map) + continue + else + //create a new z_level if needed + while (zcrd > world.maxz) + world.incrementMaxZ() + placing_template.did_expand = FALSE + if(!no_changeturf) + WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called") + // Check for tick overrun + if (TICK_CHECK) + return + +/datum/async_map_generator/map_place/proc/build_coordinate_grid_line() + // Get the current grid line + var/line = gset.gridLines[current_grid_line ++] + if((ycrd - y_offset + 1) < y_lower || (ycrd - y_offset + 1) > y_upper) //Reverse operation and check if it is out of bounds of cropping. + --ycrd + return + if(ycrd <= world.maxy && ycrd >= 1) + var/xcrd = gset.xcrd + x_offset - 1 + for(var/tpos = 1 to length(line) - placing_template.key_len + 1 step placing_template.key_len) + if((xcrd - x_offset + 1) < x_lower || (xcrd - x_offset + 1) > x_upper) //Same as above. + ++xcrd + continue //X cropping. + if(xcrd > world.maxx) + if(crop_map) + break + else + world.maxx = xcrd + placing_template.did_expand = TRUE + + if(xcrd >= 1) + var/model_key = copytext(line, tpos, tpos + placing_template.key_len) + var/no_afterchange = no_changeturf || zexpansion + if(!no_afterchange || (model_key != space_key)) + var/list/cache = model_cache[model_key] + if(!cache) + CRASH("Undefined model key in DMM: [model_key]") + placing_template.build_coordinate(area_cache, cache, locate(xcrd, ycrd, zcrd), no_afterchange, place_on_top) + + // only bother with bounds that actually exist + bounds[MAP_MINX] = min(bounds[MAP_MINX], xcrd) + bounds[MAP_MINY] = min(bounds[MAP_MINY], ycrd) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], xcrd) + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], ycrd) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) +#ifdef TESTING + else + ++placing_template.turfsSkipped +#endif + ++xcrd + --ycrd + +//====================================== +// CACHE BUILDING +//====================================== + +/// Initialize cache building +/datum/async_map_generator/map_place/proc/build_cache_start() + // Model cache is already setup + if (placing_template.modelCache) + model_cache = placing_template.modelCache + set_stage(GENERATE_STAGE_BUILD_COORDINATES_START) + return + //Set these all to be the same reference + model_cache = placing_template.modelCache = list() + set_stage(GENERATE_STAGE_BUILD_CACHE) + //Set the grid models + grid_models = placing_template.grid_models + //Set the first stage of the grid model loop + model_key = grid_models[run_stage] + build_cache_set_model_loop() + +/// Build the cache +/datum/async_map_generator/map_place/proc/build_cache() + do + // dpos loop + while (dpos != 0) + // Build this section of the cache + build_cache_construct_members() + // Tick overrun, return to MC (We will pick up where we left from) + if (TICK_CHECK) + return + //check and see if we can just skip this turf + //So you don't have to understand this horrid statement, we can do this if + // 1. no_changeturf is set + // 2. the space_key isn't set yet + // 3. there are exactly 2 members + // 4. with no attributes + // 5. and the members are world.turf and world.area + // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default) + // We can skip calling this proc every time we see XXX + if(no_changeturf \ + && !(model_cache[SPACE_KEY]) \ + && members.len == 2 \ + && members_attributes.len == 2 \ + && length(members_attributes[1]) == 0 \ + && length(members_attributes[2]) == 0 \ + && (world.area in members) \ + && (world.turf in members)) + + model_cache[SPACE_KEY] = model_key + continue + + model_cache[model_key] = list(members, members_attributes) + + // Continue until we overrun + if (TICK_CHECK) + return + while(build_cache_move_next()) + +/// Move to the next element in the build cache +/datum/async_map_generator/map_place/proc/build_cache_move_next() + run_stage ++ + //Check if we are still in range + if (run_stage > length(grid_models)) + // Out of range, cache building is completed + set_stage(GENERATE_STAGE_BUILD_COORDINATES_START) + //Store the cache in the template + placing_template.modelCache = model_cache + return FALSE + model_key = grid_models[run_stage] + build_cache_set_model_loop() + return TRUE + +/// Start of the grid_model loop +/datum/async_map_generator/map_place/proc/build_cache_set_model_loop() + model = grid_models[model_key] + members = list() + members_attributes = list() + //Reset dpos for next loop + dpos = null + index = 1 + old_position = 1 + +/// Constructing members and corresponding variables lists +/datum/async_map_generator/map_place/proc/build_cache_construct_members() + //finding next member (e.g /turf/unsimulated/wall{icon_state = "rock"} or /area/mine/explored) + //find next delimiter (comma here) that's not within {...} + dpos = placing_template.find_next_delimiter_position(model, old_position, ",", "{", "}") + //full definition, e.g : /obj/foo/bar{variables=derp} + var/full_def = trim_reduced(copytext(model, old_position, dpos)) + var/variables_start = findtext(full_def, "{") + var/path_text = trim_reduced(copytext(full_def, 1, variables_start)) + //path definition, e.g /obj/foo/bar + var/atom_def = text2path(path_text) + if(dpos) + old_position = dpos + length(model[dpos]) + + // Skip the item if the path does not exist. Fix your crap, mappers! + if(!ispath(atom_def, /atom)) + return + members.Add(atom_def) + + //transform the variables in text format into a list (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) + var/list/fields = list() + + //if there's any variable + if(variables_start) + //removing the last '}' + full_def = copytext(full_def, variables_start + length(full_def[variables_start]), -length(copytext_char(full_def, -1))) + fields = placing_template.readlist(full_def, ";") + if(fields.len) + if(!trim(fields[fields.len])) + --fields.len + for(var/I in fields) + var/value = fields[I] + if(istext(value)) + fields[I] = apply_text_macros(value) + + //then fill the members_attributes list with the corresponding variables + members_attributes.len++ + members_attributes[index++] = fields + +#undef GENERATE_STAGE_BUILD_CACHE_START +#undef GENERATE_STAGE_BUILD_CACHE +#undef GENERATE_STAGE_BUILD_COORDINATES_START +#undef GENERATE_STAGE_BUILD_COORDINATES +#undef GENERATE_STAGE_COMPLETED diff --git a/aquila/code/datums/tutorial/_tutorial.dm b/aquila/code/datums/tutorial/_tutorial.dm new file mode 100644 index 00000000000..45d87690c8b --- /dev/null +++ b/aquila/code/datums/tutorial/_tutorial.dm @@ -0,0 +1,247 @@ +GLOBAL_LIST_EMPTY_TYPED(ongoing_tutorials, /datum/tutorial) + +/datum/tutorial + /// What the tutorial is called, is player facing + var/name = "Base" + /// Internal ID of the tutorial + var/tutorial_id = "base" + /// A short 1-2 sentence description of the tutorial + var/desc = "" + /// What the tutorial's icon in the UI should look like + var/icon_state = "" + /// What category the tutorial should be under + var/category = TUTORIAL_CATEGORY_BASE + /// Ref to the bottom_left_corner + var/turf/bottom_left_corner + /// Ref to the turf reservation for this tutorial + var/datum/turf_reservation/reservation + /// Ref to the player who is doing the tutorial + var/mob/tutorial_mob + /// If the tutorial will be ending soon + var/tutorial_ending = FALSE + /// A dict of type:atom ref for some important junk that should be trackable + var/list/tracking_atoms = list() + /// What map template should be used for the tutorial + var/datum/map_template/tutorial/tutorial_template = /datum/map_template/tutorial/s12x12 + /// What is the parent path of this, to exclude from the tutorial menu + var/parent_path = /datum/tutorial + /// A dictionary of "bind_name" : "keybind_button". The inverse of `key_bindings` on a client's prefs + var/list/player_bind_dict = list() + +/datum/tutorial/Destroy(force, ...) + GLOB.ongoing_tutorials -= src + qdel(reservation) + + tutorial_mob = null // We don't delete it because the turf reservation will do that for us + + QDEL_LIST_ASSOC_VAL(tracking_atoms) + + return ..() + +/datum/tutorial/proc/init_tutorial(mob/starting_mob) + SHOULD_CALL_PARENT(TRUE) + + if(!starting_mob?.client) + return FALSE + + ADD_TRAIT(starting_mob, TRAIT_IN_TUTORIAL, TRAIT_SOURCE_TUTORIAL) + tutorial_mob = starting_mob + reservation = SSmapping.RequestBlockReservation(initial(tutorial_template.width), initial(tutorial_template.height)) + if(!reservation) + return FALSE + + var/turf/bottom_left_corner_reservation = locate(reservation.bottom_left_coords[1], reservation.bottom_left_coords[2], reservation.bottom_left_coords[3]) + var/datum/map_template/tutorial/template = new tutorial_template + to_chat(world, " [reservation.bottom_left_coords[1]], [reservation.bottom_left_coords[2]], [reservation.bottom_left_coords[3]]") + to_chat(world, " [reservation]") + to_chat(world, " [template]") + var/datum/async_map_generator/template_placer = template.load_tut(bottom_left_corner_reservation, FALSE, TRUE) + to_chat(world, " [template_placer]") + template_placer.on_completion(CALLBACK(src, PROC_REF(start_tutorial), tutorial_mob)) + +/datum/tutorial/proc/start_tutorial(mob/starting_mob) + var/obj/test_landmark = locate(/obj/effect/landmark/tutorial_bottom_left) in GLOB.landmarks_list + bottom_left_corner = get_turf(test_landmark) + qdel(test_landmark) + if(!verify_template_loaded()) + abort_tutorial() + return FALSE + + generate_binds() + + GLOB.ongoing_tutorials |= src + var/area/tutorial_area = get_area(bottom_left_corner) + init_map() + if(!tutorial_mob) + end_tutorial() + + return TRUE + +/// The proc used to end and clean up the tutorial +/datum/tutorial/proc/end_tutorial(completed = FALSE) + SHOULD_CALL_PARENT(TRUE) + + if(tutorial_mob) + var/mob/dead/new_player/NP = new() + if(!tutorial_mob.mind) + tutorial_mob.mind_initialize() + + tutorial_mob.mind.transfer_to(NP) + if(!QDELETED(src)) + qdel(src) + +/datum/tutorial/proc/verify_template_loaded() + // We subtract 1 from x and y because the bottom left corner doesn't start at the walls. + var/turf/true_bottom_left_corner = locate( + reservation.bottom_left_coords[1], + reservation.bottom_left_coords[2], + reservation.bottom_left_coords[3], + ) + // We subtract 1 from x and y here because the bottom left corner counts as the first tile + var/turf/top_right_corner = locate( + true_bottom_left_corner.x + initial(tutorial_template.width) - 1, + true_bottom_left_corner.y + initial(tutorial_template.height) - 1, + true_bottom_left_corner.z + ) + for(var/turf/tile as anything in block(true_bottom_left_corner, top_right_corner)) + // For some reason I'm unsure of, the template will not always fully load, leaving some tiles to be space tiles. So, we check all tiles in the (small) tutorial area + // and tell start_tutorial to abort if there's any space tiles. + if(istype(tile, /turf/open/space)) + return FALSE + + return TRUE + +/// Something went very, very wrong during load so let's abort +/datum/tutorial/proc/abort_tutorial() + to_chat(tutorial_mob, "Something went wrong during tutorial load, please try again!") + end_tutorial(FALSE) + +/datum/tutorial/proc/add_highlight(atom/target, color = "#d19a02") + target.add_filter("tutorial_highlight", 2, list("type" = "outline", "color" = color, "size" = 1)) + +/datum/tutorial/proc/remove_highlight(atom/target) + target.remove_filter("tutorial_highlight") + +/datum/tutorial/proc/add_to_tracking_atoms(atom/reference) + tracking_atoms[reference.type] = reference + +/datum/tutorial/proc/remove_from_tracking_atoms(atom/reference) + tracking_atoms -= reference.type + +/// Broadcast a message to the player's screen +/datum/tutorial/proc/message_to_player(message) + playsound(tutorial_mob.client, 'sound/effects/radio1.ogg', tutorial_mob.loc, 25, FALSE) + tutorial_mob.balloon_alert(tutorial_mob, message) + to_chat(tutorial_mob, "[message]") + + +/// Initialize the tutorial mob. +/datum/tutorial/proc/init_mob() + tutorial_mob.AddComponent(/datum/component/tutorial_status) +// give_action(tutorial_mob, /datum/action/tutorial_end, null, null, src) + ADD_TRAIT(tutorial_mob, TRAIT_IN_TUTORIAL, TRAIT_SOURCE_TUTORIAL) + +/// Ends the tutorial after a certain amount of time. +/datum/tutorial/proc/tutorial_end_in(time = 5 SECONDS, completed = TRUE) + tutorial_ending = TRUE + addtimer(CALLBACK(src, PROC_REF(end_tutorial), completed), time) + +/// Initialize any objects that need to be in the tutorial area from the beginning. +/datum/tutorial/proc/init_map() + return + +/// Returns a turf offset by offset_x (left-to-right) and offset_y (up-to-down) +/datum/tutorial/proc/loc_from_corner(offset_x = 0, offset_y = 0) + RETURN_TYPE(/turf) + return locate(bottom_left_corner.x + offset_x, bottom_left_corner.y + offset_y, bottom_left_corner.z) + +/// Handle the player ghosting out +/datum/tutorial/proc/on_ghost(datum/source, mob/dead/observer/ghost) + SIGNAL_HANDLER + var/mob/dead/new_player/NP = new() + if(!ghost.mind) + ghost.mind_initialize() + + ghost.mind.transfer_to(NP) + + end_tutorial(FALSE) + +/// A wrapper for signals to call end_tutorial() +/datum/tutorial/proc/signal_end_tutorial(datum/source) + SIGNAL_HANDLER + + end_tutorial(FALSE) + +/// Called whenever the tutorial_mob logs out +/datum/tutorial/proc/on_logout(datum/source) + SIGNAL_HANDLER + end_tutorial(FALSE) + +/// Generate a dictionary of button : action for use of referencing what keys to press +/datum/tutorial/proc/generate_binds() + if(!tutorial_mob.client?.prefs) + return + + for(var/bind in tutorial_mob.client.prefs.key_bindings) + var/action = tutorial_mob.client.prefs.key_bindings[bind] + // We presume the first action under a certain binding is the one we want. + if(action[1] in player_bind_dict) + player_bind_dict[bind] += action[1] + else + player_bind_dict[bind] = list(action[1]) + +/// Getter for player_bind_dict. Provide an action name like "North" or "quick_equip" +/datum/tutorial/proc/retrieve_bind(action_name) + if(!action_name) + return + + if(!(action_name in player_bind_dict)) + return "Undefined" + + return player_bind_dict[action_name][1] + +/datum/action/tutorial_end + name = "Stop Tutorial" +// icon_state = "nose" + /// Weakref to the tutorial this is related to + var/datum/weakref/tutorial + +/datum/action/tutorial_end/New(Target, override_icon_state, datum/tutorial/selected_tutorial) + . = ..() + tutorial = WEAKREF(selected_tutorial) + +/datum/action/tutorial_end/proc/action_activate() + if(!tutorial) + return + + var/datum/tutorial/selected_tutorial = tutorial.resolve() + if(selected_tutorial.tutorial_ending) + return + + selected_tutorial.end_tutorial() + +/datum/map_template/tutorial + name = "Tutorial Zone (12x12)" + mappath = "_maps/tutorial/tutorial_12x12.dmm" + width = 12 + height = 12 + +/datum/map_template/tutorial/s12x12 + +/datum/map_template/tutorial/s12x12 + +/datum/map_template/tutorial/s8x9 + name = "Tutorial Zone (8x9)" + mappath = "maps/tutorial/tutorial_8x9.dmm" + width = 8 + height = 9 + +/datum/map_template/tutorial/s8x9/no_baselight + name = "Tutorial Zone (8x9) (No Baselight)" + mappath = "maps/tutorial/tutorial_8x9_nb.dmm" + +/datum/map_template/tutorial/s7x7 + name = "Tutorial Zone (7x7)" + mappath = "maps/tutorial/tutorial_7x7.dmm" + width = 7 + height = 7 diff --git a/aquila/code/datums/tutorial/_tutorial_menu.dm b/aquila/code/datums/tutorial/_tutorial_menu.dm new file mode 100644 index 00000000000..4b1346c1f44 --- /dev/null +++ b/aquila/code/datums/tutorial/_tutorial_menu.dm @@ -0,0 +1,76 @@ +/datum/tutorial_menu + /// List of ["name" = name, "tutorials" = ["name" = name, "path" = "path", "id" = tutorial_id]] + var/static/list/categories = list() + +/datum/tutorial_menu/New() + if(!length(categories)) + var/list/categories_2 = list() + for(var/datum/tutorial/tutorial as anything in subtypesof(/datum/tutorial)) + if(initial(tutorial.parent_path) == tutorial) + continue + + if(!(initial(tutorial.category) in categories_2)) + categories_2[initial(tutorial.category)] = list() + + categories_2[initial(tutorial.category)] += list(list( + "name" = initial(tutorial.name), + "path" = "[tutorial]", + "id" = initial(tutorial.tutorial_id), + "description" = initial(tutorial.desc), + "image" = initial(tutorial.icon_state), + )) + + for(var/category in categories_2) + categories += list(list( + "name" = category, + "tutorials" = categories_2[category], + )) + + +/datum/tutorial_menu/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TutorialMenu") + ui.open() + + +/datum/tutorial_menu/ui_state(mob/user) + if(istype(get_area(user), /area/tutorial)) + return GLOB.never_state + + return GLOB.new_player_state + + +/datum/tutorial_menu/ui_static_data(mob/user) + var/list/data = list() + + data["tutorial_categories"] = categories + + return data + + +/datum/tutorial_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("select_tutorial") + var/datum/tutorial/path + if(!params["tutorial_path"]) + return + + path = text2path(params["tutorial_path"]) + + if(!path || !isnewplayer(usr)) + return + + if(HAS_TRAIT(usr, TRAIT_IN_TUTORIAL) || istype(get_area(usr), /area/tutorial)) + to_chat(usr, " You are currently in a tutorial, or one is loading. Please be patient.") + return + + path = new path + var/mob/dead/new_player/new_player = usr + new_player.close_spawn_windows() + path.init_tutorial(usr) + return TRUE diff --git a/aquila/code/datums/tutorial/ss13/_ss13.dm b/aquila/code/datums/tutorial/ss13/_ss13.dm new file mode 100644 index 00000000000..d9e32bb259e --- /dev/null +++ b/aquila/code/datums/tutorial/ss13/_ss13.dm @@ -0,0 +1,13 @@ +/datum/tutorial/ss13 + category = TUTORIAL_CATEGORY_SS13 + parent_path = /datum/tutorial/ss13 + icon_state = "ss13" + +/datum/tutorial/ss13/init_mob() + + var/mob/living/carbon/human/new_character = new(bottom_left_corner) + if(tutorial_mob.mind) + tutorial_mob.mind_initialize() + tutorial_mob.mind.transfer_to(new_character, TRUE) + tutorial_mob = new_character + return ..() diff --git a/aquila/code/datums/tutorial/ss13/basic_ss13.dm b/aquila/code/datums/tutorial/ss13/basic_ss13.dm new file mode 100644 index 00000000000..d58d9f506af --- /dev/null +++ b/aquila/code/datums/tutorial/ss13/basic_ss13.dm @@ -0,0 +1,57 @@ +/datum/tutorial/ss13/basic + name = "Space Station 13 - Basic" + desc = "Learn the very basics of Space Station 13. Recommended if you haven't played before." + tutorial_id = "ss13_basic_1" + tutorial_template = /datum/map_template/tutorial/s12x12 + +/datum/tutorial/ss13/basic/start_tutorial(mob/starting_mob) + . = ..() + init_mob() + + message_to_player("This is the tutorial for the basics of Space Station 13.") + addtimer(CALLBACK(src, PROC_REF(require_move)), 4 SECONDS) // check if this is a good amount of time + +/datum/tutorial/ss13/basic/proc/require_move() + message_to_player("Now, move in any direction using [retrieve_bind("move_north")], [retrieve_bind("move_west")], [retrieve_bind("move_south")], or [retrieve_bind("move_east")].") + + RegisterSignal(tutorial_mob, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) + +/datum/tutorial/ss13/basic/proc/on_move(datum/source, actually_moving, direction, specific_direction) + SIGNAL_HANDLER + + UnregisterSignal(tutorial_mob, COMSIG_MOVABLE_MOVED) + + message_to_player("Good. Now, switch hands with [retrieve_bind("swap_hands")].") + RegisterSignal(tutorial_mob, COMSIG_MOB_SWAP_HANDS, PROC_REF(on_hand_swap)) + +/datum/tutorial/ss13/basic/proc/on_hand_swap(datum/source) + SIGNAL_HANDLER + + UnregisterSignal(tutorial_mob, COMSIG_MOB_SWAP_HANDS) + + message_to_player("Good. Now, pick up the backpack that just spawned and equip it with [retrieve_bind("quick_equip")].") + + var/obj/item/storage/backpack/backpack = new(loc_from_corner(2, 2)) +// add_to_tracking_atoms(satchel) +// add_highlight(satchel) + + RegisterSignal(tutorial_mob, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_backpack_equip)) + +/datum/tutorial/ss13/basic/proc/on_backpack_equip(datum/source, obj/item/equiped, slot) + SIGNAL_HANDLER + + if(slot != ITEM_SLOT_BACK) + return + + UnregisterSignal(tutorial_mob, COMSIG_MOB_EQUIPPED_ITEM) + + message_to_player("Nice, say anything by pressing [retrieve_bind("say")] and typing in the chat box.") + RegisterSignal(tutorial_mob, COMSIG_MOB_SAY, PROC_REF(on_say)) + +/datum/tutorial/ss13/basic/proc/on_say(datum/source) + SIGNAL_HANDLER + + UnregisterSignal(tutorial_mob, COMSIG_MOB_SAY) + + message_to_player("Good. The next tutorial will cover intents. The tutirial will end shortly.") + tutorial_end_in(5 SECONDS, TRUE) diff --git a/aquila/code/datums/tutorial/ss13/intents_ss13.dm b/aquila/code/datums/tutorial/ss13/intents_ss13.dm new file mode 100644 index 00000000000..5123fe993bd --- /dev/null +++ b/aquila/code/datums/tutorial/ss13/intents_ss13.dm @@ -0,0 +1,35 @@ +/datum/tutorial/ss13/intents + name = "Space Station 13 - Intents" + desc = "Learn how the intent interaction system works." + tutorial_id = "ss13_intents_1" + tutorial_template = /datum/map_template/tutorial/s12x12 + +/datum/tutorial/ss13/intents/start_tutorial(mob/starting_mob) + . = ..() + if(!.) + return + + init_mob() + message_to_player("This is the tutorial for the intents system of Space Station 13. The highlighted UI element in the bottom-right corner is your current intent.") +// var/datum/hud/human/human_hud = tutorial_mob.hud_used +// add_highlight(human_hud.action_intent) + + addtimer(CALLBACK(src, PROC_REF(require_help)), 4.5 SECONDS) + +/datum/tutorial/ss13/intents/proc/require_help() + tutorial_mob.a_intent_change(INTENT_DISARM) + message_to_player("Your intent has been changed off of help. Change back to it by pressing [retrieve_bind("select_help_intent")].") +// RegisterSignal(tutorial_mob, COMSIG_MOB_INTENT_CHANGE, PROC_REF(on_help_intent)) + +/datum/tutorial/ss13/intents/proc/on_help_intent(datum/source, new_intent) + SIGNAL_HANDLER + + if(new_intent != INTENT_HELP) + return + + // UnregisterSignal(tutorial_mob, COMSIG_MOB_INTENT_CHANGE) + + var/mob/living/carbon/human/dummy/tutorial_dummy = new(loc_from_corner(2, 3)) + message_to_player("The first of the intents is help intent. It is used to harmlessly touch others, put out fire, give CPR, and similar. Click on the Test Dummy to give them a hug.") + +// RegisterSignal(tutorial_mob, COMSIG_MOB_ATTACK_HAND, PROC_REF(on_help_attack)) diff --git a/aquila/code/game/area/Space_Station_13_areas.dm b/aquila/code/game/area/Space_Station_13_areas.dm new file mode 100644 index 00000000000..621ff2e55ca --- /dev/null +++ b/aquila/code/game/area/Space_Station_13_areas.dm @@ -0,0 +1,6 @@ +/area/tutorial + name = "Tutorial Zone" + icon_state = "tutorial" + requires_power = FALSE + has_gravity = STANDARD_GRAVITY + area_flags = UNIQUE_AREA diff --git a/aquila/code/game/objects/effects/landmarks.dm b/aquila/code/game/objects/effects/landmarks.dm index 899d0898908..240f5c28643 100644 --- a/aquila/code/game/objects/effects/landmarks.dm +++ b/aquila/code/game/objects/effects/landmarks.dm @@ -17,3 +17,8 @@ ..() GLOB.infiltrator_objective_items += loc return INITIALIZE_HINT_QDEL + +/// Marks the bottom left of the tutorial zone. +/obj/effect/landmark/tutorial_bottom_left + name = "tutorial bottom left" + diff --git a/aquila/code/modules/mapping/map_template.dm b/aquila/code/modules/mapping/map_template.dm new file mode 100644 index 00000000000..81e9363d0ba --- /dev/null +++ b/aquila/code/modules/mapping/map_template.dm @@ -0,0 +1,59 @@ +/datum/map_template + var/maps_loading = 0 + +/datum/map_template/proc/load_tut(turf/T, centered = FALSE, init_atmos = TRUE, finalize = TRUE, ...) + if(centered) + T = locate(T.x - round(width/2) , T.y - round(height/2) , T.z) + if(!T) + return + if(T.x+width > world.maxx) + return + if(T.y+height > world.maxy) + return + + var/list/border = block(locate(max(T.x, 1), max(T.y, 1), T.z), + locate(min(T.x+width, world.maxx), min(T.y+height, world.maxy), T.z)) + for(var/L in border) + var/turf/turf_to_disable = L + turf_to_disable.ImmediateDisableAdjacency() + + // Accept cached maps, but don't save them automatically - we don't want + // ruins clogging up memory for the whole round. + maps_loading ++ + var/datum/parsed_map/parsed = cached_map ? cached_map.copy() : new(file(mappath)) + cached_map = parsed + + var/list/turf_blacklist = list() + update_blacklist(T, turf_blacklist) + + UNSETEMPTY(turf_blacklist) + parsed.turf_blacklist = turf_blacklist + var/datum/async_map_generator/map_place/map_placer = new(parsed, T.x, T.y, T.z, cropMap=TRUE, no_changeturf=(SSatoms.initialized == INITIALIZATION_INSSATOMS), placeOnTop=should_place_on_top) + map_placer.on_completion(CALLBACK(src, PROC_REF(on_placement_completed))) + var/list/generation_arguments = list(T, init_atmos, parsed, finalize) + if (length(args) > 4) + generation_arguments += args.Copy(5) + map_placer.generate(arglist(generation_arguments)) + return map_placer + +/datum/map_template/proc/on_placement_completed(datum/async_map_generator/map_gen, turf/T, init_atmos, datum/parsed_map/parsed, finalize = TRUE, ...) + var/list/bounds = parsed.bounds + if(!bounds) + maps_loading -- + if (!maps_loading) + cached_map = keep_cached_map ? parsed : null + message_admins("NO PARSED BOUNDS!") + return + + require_area_resort() + + //If this is a superfunction call, we don't want to initialize atoms here, let the subfunction handle that + if(finalize) + maps_loading -- + if (!maps_loading) + cached_map = keep_cached_map ? parsed : null + //initialize things that are normally initialized after map load + initTemplateBounds(bounds, init_atmos) + log_game("[name] loaded at [T.x],[T.y],[T.z]") + + return bounds diff --git a/aquila/code/modules/mapping/reader.dm b/aquila/code/modules/mapping/reader.dm new file mode 100644 index 00000000000..1e33f4a9eac --- /dev/null +++ b/aquila/code/modules/mapping/reader.dm @@ -0,0 +1,10 @@ +/datum/parsed_map/proc/copy() + var/datum/parsed_map/copy = new() + copy.original_path = original_path + copy.key_len = key_len + copy.grid_models = grid_models + copy.gridSets = gridSets + copy.modelCache = modelCache + copy.parsed_bounds = parsed_bounds + copy.turf_blacklist = list() + return copy diff --git a/aquila/icons/misc/tutorial.dmi b/aquila/icons/misc/tutorial.dmi new file mode 100644 index 00000000000..74c664ba103 Binary files /dev/null and b/aquila/icons/misc/tutorial.dmi differ diff --git a/code/__HELPERS/areas.dm b/code/__HELPERS/areas.dm index 69c808d9ef0..6fcdc4328c5 100644 --- a/code/__HELPERS/areas.dm +++ b/code/__HELPERS/areas.dm @@ -116,6 +116,7 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( #undef BP_MAX_ROOM_SIZE + //Repopulates sortedAreas list /proc/repopulate_sorted_areas() GLOB.sortedAreas = list() diff --git a/code/modules/admin/verbs/map_template_loadverb.dm b/code/modules/admin/verbs/map_template_loadverb.dm index 82db4dc885a..dda84d45359 100644 --- a/code/modules/admin/verbs/map_template_loadverb.dm +++ b/code/modules/admin/verbs/map_template_loadverb.dm @@ -20,12 +20,17 @@ preview += item images += preview if(alert(src,"Confirm location.","Template Confirm","Yes","No") == "Yes") + var/datum/async_map_generator/template_placer = template.load(T, centered = TRUE) + template_placer.on_completion(CALLBACK(src, PROC_REF(after_map_load), template.name)) if(template.load(T, centered = TRUE)) message_admins("[key_name_admin(src)] has placed a map template ([template.name]) at [ADMIN_COORDJMP(T)]") else to_chat(src, "Failed to place map") images -= preview +/client/proc/after_map_load(template_name, datum/async_map_generator/map_place/async_map_generator, turf/T) + message_admins("[key_name_admin(src)] has placed a map template ([template_name]) at [ADMIN_COORDJMP(T)]") + /client/proc/map_template_upload() set category = "Debug" set name = "Map Template - Upload" diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index ccdc191b8cc..e3fa719e89f 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -39,13 +39,14 @@ return /mob/dead/new_player/proc/new_player_panel() + var/output = "

Samouczek

" if (client?.interviewee) return var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/lobby) if(!asset_datum.send(client)) return - var/output = "

Ustaw Postać

" + output += "

Ustaw Postać

" if(SSticker.current_state <= GAME_STATE_PREGAME) switch(ready) @@ -201,6 +202,11 @@ var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls vote_on_poll_handler(poll, href_list) + if(href_list["show_tutorial"]) + var/datum/tutorial_menu/menu = new(src) + menu.ui_interact(src) + return + //When you cop out of the round (NB: this HAS A SLEEP FOR PLAYER INPUT IN IT) /mob/dead/new_player/proc/make_me_an_observer(force_observe=FALSE) if(QDELETED(src) || !src.client) diff --git a/tgui/packages/tgui/interfaces/TutorialMenu.tsx b/tgui/packages/tgui/interfaces/TutorialMenu.tsx new file mode 100644 index 00000000000..bd2ee748a98 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TutorialMenu.tsx @@ -0,0 +1,116 @@ +import { classes } from 'common/react'; +import { useBackend, useLocalState } from '../backend'; +import { Section, Stack, Box, Divider, Button, Tabs } from '../components'; +import { Window } from '../layouts'; + +type Tutorial = { + name: string; + path: string; + id: string; + description: string; + image: string; +}; + +type TutorialCategory = { + tutorials: Tutorial[]; + name: string; +}; + +type BackendContext = { + tutorial_categories: TutorialCategory[]; + completed_tutorials: string[]; +}; + +export const TutorialMenu = (props, context) => { + const { data, act } = useBackend(context); + const { tutorial_categories, completed_tutorials } = data; + const [chosenTutorial, setTutorial] = useLocalState(context, 'tutorial', null); + const [categoryIndex, setCategoryIndex] = useLocalState(context, 'category_index', 'Space Station 13'); + return ( + + + + + + + {tutorial_categories.map((item, key) => ( + { + setCategoryIndex(item.name); + }}> + {item.name} + + ))} + + + + + +
+ {tutorial_categories.map( + (tutorial_category) => + tutorial_category.name === categoryIndex + && tutorial_category.tutorials.map((tutorial) => ( +
+ +
+ )) + )} +
+
+ + +
+ {chosenTutorial !== null ? ( + + +
+ + + +
+
+ {chosenTutorial.description} + + +
+
+
+
+
+
+ ); +};