diff --git a/code/datums/tutorial/_tutorial.dm b/code/datums/tutorial/_tutorial.dm index 887971a33854..d68ae551b6d5 100644 --- a/code/datums/tutorial/_tutorial.dm +++ b/code/datums/tutorial/_tutorial.dm @@ -6,6 +6,10 @@ GLOBAL_LIST_EMPTY(ongoing_tutorials) var/name = "Base" /// Internal ID of the tutorial, kept for save files var/tutorial_id = "base" + /// A short 1-2 sentence description of the tutorial itself + 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 tile of the tutorial room @@ -81,7 +85,7 @@ GLOBAL_LIST_EMPTY(ongoing_tutorials) REMOVE_TRAIT(tutorial_mob, TRAIT_IN_TUTORIAL, TRAIT_SOURCE_TUTORIAL) if(tutorial_mob.client?.prefs && completed) tutorial_mob.client.prefs.completed_tutorials |= tutorial_id - tutorial_mob.client.prefs.save_preferences() + tutorial_mob.client.prefs.save_character() var/mob/new_player/new_player = new if(!tutorial_mob.mind) tutorial_mob.mind_initialize() diff --git a/code/datums/tutorial/_tutorial_menu.dm b/code/datums/tutorial/_tutorial_menu.dm index e2463535c37c..42eb3f6aabfa 100644 --- a/code/datums/tutorial/_tutorial_menu.dm +++ b/code/datums/tutorial/_tutorial_menu.dm @@ -17,6 +17,8 @@ "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) @@ -29,9 +31,13 @@ /datum/tutorial_menu/proc/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "TutorialList") + ui = new(user, src, "TutorialMenu") ui.open() +/datum/tutorial_menu/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/tutorial), + ) /datum/tutorial_menu/ui_state(mob/user) if(istype(get_area(user), /area/misc/tutorial)) diff --git a/code/datums/tutorial/marine/_marine.dm b/code/datums/tutorial/marine/_marine.dm index e7e4946b5b15..071194491f93 100644 --- a/code/datums/tutorial/marine/_marine.dm +++ b/code/datums/tutorial/marine/_marine.dm @@ -1,6 +1,7 @@ /datum/tutorial/marine category = TUTORIAL_CATEGORY_MARINE parent_path = /datum/tutorial/marine + icon_state = "marine" /datum/tutorial/marine/init_mob() tutorial_mob.close_spawn_windows() diff --git a/code/datums/tutorial/marine/basic_marine.dm b/code/datums/tutorial/marine/basic_marine.dm index d98c94111a92..db30653454c5 100644 --- a/code/datums/tutorial/marine/basic_marine.dm +++ b/code/datums/tutorial/marine/basic_marine.dm @@ -1,5 +1,6 @@ /datum/tutorial/marine/basic name = "Marine - Basic" + desc = "A tutorial to get you acquainted with the very basics of how to play a groundside marine role." tutorial_id = "marine_basic_1" tutorial_template = /datum/map_template/tutorial/s8x9 /// How many items need to be vended from the clothing vendor for the script to continue, if something vends 2 items (for example), increase this number by 2. diff --git a/code/datums/tutorial/marine/medical_basic.dm b/code/datums/tutorial/marine/medical_basic.dm index 2ec7437ff832..c5b872db43be 100644 --- a/code/datums/tutorial/marine/medical_basic.dm +++ b/code/datums/tutorial/marine/medical_basic.dm @@ -1,5 +1,6 @@ /datum/tutorial/marine/medical_basic name = "Marine - Medical (Basic)" + desc = "Learn how to treat common injuries you may face as a marine." tutorial_id = "marine_medical_1" tutorial_template = /datum/map_template/tutorial/s7x7 diff --git a/code/datums/tutorial/ss13/_ss13.dm b/code/datums/tutorial/ss13/_ss13.dm index dd66b9be03a5..625ee3220197 100644 --- a/code/datums/tutorial/ss13/_ss13.dm +++ b/code/datums/tutorial/ss13/_ss13.dm @@ -1,6 +1,7 @@ /datum/tutorial/ss13 category = TUTORIAL_CATEGORY_SS13 parent_path = /datum/tutorial/ss13 + icon_state = "ss13" /datum/tutorial/ss13/init_mob() tutorial_mob.close_spawn_windows() diff --git a/code/datums/tutorial/ss13/basic_ss13.dm b/code/datums/tutorial/ss13/basic_ss13.dm index 00717e76b29e..d27c990b9799 100644 --- a/code/datums/tutorial/ss13/basic_ss13.dm +++ b/code/datums/tutorial/ss13/basic_ss13.dm @@ -1,5 +1,6 @@ /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/s7x7 diff --git a/code/datums/tutorial/ss13/intents.dm b/code/datums/tutorial/ss13/intents.dm index b19510dc2896..d47a07031ef9 100644 --- a/code/datums/tutorial/ss13/intents.dm +++ b/code/datums/tutorial/ss13/intents.dm @@ -1,5 +1,7 @@ /datum/tutorial/ss13/intents name = "Space Station 13 - Intents" + desc = "Learn how the intent interaction system works." + icon_state = "intents" tutorial_id = "ss13_intents_1" tutorial_template = /datum/map_template/tutorial/s7x7 diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index d5d44a047947..2279601d66f3 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -376,6 +376,22 @@ Insert("[icon_name]_big", iconBig) return ..() +/datum/asset/spritesheet/tutorial + name = "tutorial" + +/datum/asset/spritesheet/tutorial/register() + for(var/icon_state in icon_states('icons/misc/tutorial.dmi')) + var/icon/icon_sprite = icon('icons/misc/tutorial.dmi', icon_state) + icon_sprite.Scale(128, 128) + Insert(icon_state, icon_sprite) + + var/icon/retrieved_icon = icon('icons/mob/hud/human_dark.dmi', "intent_all") + retrieved_icon.Scale(128, 128) + Insert("intents", retrieved_icon) + + return ..() + + /datum/asset/spritesheet/gun_lineart name = "gunlineart" diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index fbabf2a90707..e29d48eccd32 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -163,6 +163,11 @@ to_chat(src, SPAN_WARNING("Sorry, you cannot late join during [SSticker.mode.name]. You have to start at the beginning of the round. You may observe or try to join as an alien, if possible.")) return + if(client.player_data?.playtime_loaded && (client.get_total_human_playtime() < CONFIG_GET(number/notify_new_player_age)) && !length(client.prefs.completed_tutorials)) + if(tgui_alert(src, "You have little playtime and haven't completed any tutorials. Would you like to go to the tutorial menu?", "Tutorial", list("Yes", "No")) == "Yes") + tutorial_menu() + return + if(client.prefs.species != "Human") if(!is_alien_whitelisted(src, client.prefs.species) && CONFIG_GET(flag/usealienwhitelist)) to_chat(src, "You are currently not whitelisted to play [client.prefs.species].") @@ -226,20 +231,22 @@ return if("tutorial") - if(SSticker.current_state <= GAME_STATE_SETTING_UP) - to_chat(usr, SPAN_WARNING("Please wait for the round to start before entering a tutorial.")) - return - - if(SSticker.current_state == GAME_STATE_FINISHED) - to_chat(usr, SPAN_WARNING("The round has ended. Please wait for the next round to enter a tutorial.")) - return - - var/datum/tutorial_menu/menu = new(usr) - menu.ui_interact(usr) + tutorial_menu() else new_player_panel() +/mob/new_player/proc/tutorial_menu() + if(SSticker.current_state <= GAME_STATE_SETTING_UP) + to_chat(src, SPAN_WARNING("Please wait for the round to start before entering a tutorial.")) + return + + if(SSticker.current_state == GAME_STATE_FINISHED) + to_chat(src, SPAN_WARNING("The round has ended. Please wait for the next round to enter a tutorial.")) + return + + var/datum/tutorial_menu/menu = new(src) + menu.ui_interact(src) /mob/new_player/proc/AttemptLateSpawn(rank) var/datum/job/player_rank = RoleAuthority.roles_for_mode[rank] @@ -293,7 +300,7 @@ var/client/C = character.client if(C.player_data && C.player_data.playtime_loaded && length(C.player_data.playtimes) == 0) msg_admin_niche("NEW PLAYER: [key_name(character, 1, 1, 0)]. IP: [character.lastKnownIP], CID: [character.computer_id]") - if(C.player_data && C.player_data.playtime_loaded && ((round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1)) <= 5)) + if(C.player_data && C.player_data.playtime_loaded && ((round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1)) <= CONFIG_GET(number/notify_new_player_age))) msg_sea("NEW PLAYER: [key_name(character, 0, 1, 0)] only has [(round(C.get_total_human_playtime() DECISECONDS_TO_HOURS, 0.1))] hours as a human. Current role: [get_actual_job_name(character)] - Current location: [get_area(character)]") character.client.init_verbs() diff --git a/icons/misc/tutorial.dmi b/icons/misc/tutorial.dmi new file mode 100644 index 000000000000..d4a4e65963ba Binary files /dev/null and b/icons/misc/tutorial.dmi differ diff --git a/tgui/packages/tgui/interfaces/TutorialMenu.tsx b/tgui/packages/tgui/interfaces/TutorialMenu.tsx new file mode 100644 index 000000000000..a3ff8827d482 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TutorialMenu.tsx @@ -0,0 +1,147 @@ +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) => ( + + setTutorial(tutorial)}> + {tutorial.name} + + + )) + )} + + + + + + {chosenTutorial !== null ? ( + + + + + + + + + {chosenTutorial.description} + {completed_tutorials.indexOf(chosenTutorial.id) === -1 ? ( + + ) : ( + + Tutorial has been completed. + + )} + + + act('select_tutorial', { + tutorial_path: chosenTutorial.path, + }) + } + /> + + + ) : ( + + )} + + + + + + + ); +};