diff --git a/_maps/tutorial/tutorial_12x12.dmm b/_maps/tutorial/tutorial_12x12.dmm
new file mode 100644
index 00000000000..64e37bc0051
--- /dev/null
+++ b/_maps/tutorial/tutorial_12x12.dmm
@@ -0,0 +1,197 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"a" = (
+/turf/closed/wall/mineral/craftstone,
+/area)
+"e" = (
+/obj/machinery/light/fueled/torchholder/metal_torch/east,
+/turf/open/floor/blocks/newstone,
+/area)
+"n" = (
+/turf/open/floor/blocks/newstone,
+/area)
+"x" = (
+/obj/machinery/light/fueled/torchholder/metal_torch/east,
+/obj/effect/landmark/tutorial_bottom_left,
+/turf/open/floor/blocks/newstone,
+/area)
+"N" = (
+/obj/machinery/light/fueled/torchholder/metal_torch/west,
+/turf/open/floor/blocks/newstone,
+/area)
+"P" = (
+/obj/machinery/light/fueled/torchholder/metal_torch/north,
+/turf/open/floor/blocks/newstone,
+/area)
+"S" = (
+/obj/machinery/light/fueled/torchholder/c,
+/turf/open/floor/blocks/newstone,
+/area)
+
+(1,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(2,1,1) = {"
+a
+e
+n
+n
+e
+n
+n
+e
+n
+n
+x
+a
+"}
+(3,1,1) = {"
+a
+n
+n
+n
+n
+n
+n
+n
+n
+n
+n
+a
+"}
+(4,1,1) = {"
+a
+n
+n
+n
+n
+n
+n
+n
+n
+n
+n
+a
+"}
+(5,1,1) = {"
+a
+S
+n
+n
+n
+n
+n
+n
+n
+n
+P
+a
+"}
+(6,1,1) = {"
+a
+n
+n
+n
+n
+n
+n
+n
+n
+n
+n
+a
+"}
+(7,1,1) = {"
+a
+n
+n
+n
+n
+n
+n
+n
+n
+n
+n
+a
+"}
+(8,1,1) = {"
+a
+S
+n
+n
+n
+n
+n
+n
+n
+n
+P
+a
+"}
+(9,1,1) = {"
+a
+n
+n
+n
+n
+n
+n
+n
+n
+n
+n
+a
+"}
+(10,1,1) = {"
+a
+n
+n
+n
+n
+n
+n
+n
+n
+n
+n
+a
+"}
+(11,1,1) = {"
+a
+N
+n
+n
+N
+n
+n
+N
+n
+n
+N
+a
+"}
+(12,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
diff --git a/code/__DEFINES/dcs/signals/signals_atoms/signals_obj.dm b/code/__DEFINES/dcs/signals/signals_atoms/signals_obj.dm
index eed06357064..3bf684af38c 100644
--- a/code/__DEFINES/dcs/signals/signals_atoms/signals_obj.dm
+++ b/code/__DEFINES/dcs/signals/signals_atoms/signals_obj.dm
@@ -82,3 +82,5 @@
/// from base of obj/item/reagent_containers/food/snacks/attack(): (mob/living/eater, mob/feeder)
#define COMSIG_FOOD_EATEN "food_eaten"
+
+#define COMSIG_CLOTH_SOAKED "cloth_soaked"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index 2a2693ea109..9af3c31b0a5 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -144,6 +144,8 @@
#define COMSIG_ITEM_DROPPED "item_drop"
///from base of obj/item/pickup(): (/mob/taker)
#define COMSIG_ITEM_PICKUP "item_pickup"
+///from base of obj/item/pickup(): (src)
+#define COMSIG_MOB_PICKUP_ITEM "mob_item_pickup"
///from base of obj/item/afterpickup(): (/mob/taker)
#define COMSIG_ITEM_AFTER_PICKUP "item_after_pickup"
///from base of mob/living/carbon/attacked_by(): (mob/living/carbon/target, mob/living/user, hit_zone)
@@ -155,3 +157,9 @@
#define COMSIG_ITEM_WEARERCROSSED "wearer_crossed"
#define COMSIG_MOB_MOUSE_ENTERED "user_mouse_entered"
+
+#define COMSIG_MOB_END_TUTORIAL "tutorial_over"
+/// From /datum/tutorial/proc/update_objective() : (new_objective)
+#define COMSIG_MOB_TUTORIAL_UPDATE_OBJECTIVE "mob_tutorial_update_objective"
+
+#define COMSIG_MOB_UNBANDAGE "mob_unbandage_limb"
diff --git a/code/__DEFINES/dcs/signals/signals_organ.dm b/code/__DEFINES/dcs/signals/signals_organ.dm
index 29c974f6a60..b087778f464 100644
--- a/code/__DEFINES/dcs/signals/signals_organ.dm
+++ b/code/__DEFINES/dcs/signals/signals_organ.dm
@@ -6,3 +6,12 @@
#define COMSIG_CHIMERIC_ORGAN_TRIGGER "chimeric_organ_trigger"
#define COMSIG_BODYPART_WOUND_REMOVED "bodypart_wound_removed"
+
+/// Called when a bodypart is successfully bandaged
+#define COMSIG_BODYPART_BANDAGED "bodypart_bandaged"
+/// Called when a bodypart is disinfected
+#define COMSIG_BODYPART_DISINFECTED "bodypart_disinfected"
+/// Called when an injury is salved
+#define COMSIG_INJURY_SALVED "injury_salved"
+/// Called when an injury is sutured
+#define COMSIG_INJURY_SUTURED "injury_sutured"
diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm
index 548351ced8a..f0b0afddf09 100644
--- a/code/__DEFINES/traits/definitions.dm
+++ b/code/__DEFINES/traits/definitions.dm
@@ -41,6 +41,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
// ************* mob traits
+/// If the mob is currently loading a tutorial
+#define TRAIT_IN_TUTORIAL "t_IN_TUTORIAL"
/// Prevents voluntary movement.
#define TRAIT_IMMOBILIZED "immobilized"
/// Buckling yourself to objects with this trait won't immobilize you
@@ -608,3 +610,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
// genetic traits
#define TRAIT_ANIMAL_NATURAL_ARMOR "natural_armor"
#define TRAIT_ANIMAL_PRODUCTIVE "trait_productive"
+
+/// Status trait coming from a tutorial
+#define TRAIT_SOURCE_TUTORIAL "t_s_tutorials"
diff --git a/code/__DEFINES/tutorial.dm b/code/__DEFINES/tutorial.dm
new file mode 100644
index 00000000000..c504570bf94
--- /dev/null
+++ b/code/__DEFINES/tutorial.dm
@@ -0,0 +1,4 @@
+#define TUTORIAL_ATOM_FROM_TRACKING(path, varname) var##path/##varname = tracking_atoms[##path]
+
+#define TUTORIAL_CATEGORY_BASE "Base" // Shouldn't be used outside of base types
+#define TUTORIAL_CATEGORY_VANDERLIN "Vanderlin"
diff --git a/code/__HELPERS/areas.dm b/code/__HELPERS/areas.dm
index f7d72cb4aa9..f0bd3deff31 100644
--- a/code/__HELPERS/areas.dm
+++ b/code/__HELPERS/areas.dm
@@ -104,3 +104,13 @@
return TRUE
#undef BP_MAX_ROOM_SIZE
+
+
+/// Returns TRUE if the target is somewhere that the game should not interact with if possible
+/// In this case, admin Zs and tutorial areas
+/proc/should_block_game_interaction(atom/target)
+ var/area/target_area = get_area(target)
+ if(target_area?.block_game_interaction)
+ return TRUE
+
+ return FALSE
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 02f24623c28..0fc29c80289 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -102,6 +102,8 @@ SUBSYSTEM_DEF(ticker)
/// ID of round reboot timer, if it exists
var/reboot_timer = null
+ ///if we don't want to create tutorial zones
+ var/tutorial_disabled = FALSE
/datum/controller/subsystem/ticker/Initialize(timeofday)
load_mode()
diff --git a/code/datums/components/tutorial_status.dm b/code/datums/components/tutorial_status.dm
new file mode 100644
index 00000000000..97b8d408bcb
--- /dev/null
+++ b/code/datums/components/tutorial_status.dm
@@ -0,0 +1,25 @@
+/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/RegisterWithParent()
+ ..()
+ RegisterSignal(parent, COMSIG_MOB_TUTORIAL_UPDATE_OBJECTIVE, PROC_REF(update_objective))
+ RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item))
+
+/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/code/datums/injury/_injury.dm b/code/datums/injury/_injury.dm
index 6e54b16b73f..70c79d9ba44 100644
--- a/code/datums/injury/_injury.dm
+++ b/code/datums/injury/_injury.dm
@@ -322,6 +322,7 @@
// salves the injury
/datum/injury/proc/salve_injury()
injury_flags |= INJURY_SALVED
+ SEND_SIGNAL(src, COMSIG_INJURY_SALVED)
return TRUE
// unsalves the injury
@@ -344,6 +345,7 @@
injury_flags |= INJURY_SUTURED
if(parent_bodypart?.spilled)
parent_bodypart.spilled = FALSE
+ SEND_SIGNAL(src, COMSIG_INJURY_SUTURED)
return TRUE
// unsutures the injury
diff --git a/code/datums/tutorial/_base_tutorial.dm b/code/datums/tutorial/_base_tutorial.dm
new file mode 100644
index 00000000000..1b69d6e48b8
--- /dev/null
+++ b/code/datums/tutorial/_base_tutorial.dm
@@ -0,0 +1,256 @@
+GLOBAL_LIST_EMPTY_TYPED(ongoing_tutorials, /datum/tutorial)
+
+/// A tutorial datum contains a set of instructions for a player tutorial, such as what to spawn, what's scripted to occur, and so on.
+/datum/tutorial
+ /// What the tutorial is called, is player facing
+ 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
+ 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_NULL(reservation) // Its Destroy() handles releasing reserved turfs
+
+ tutorial_mob = null // We don't delete it because the turf reservation will typically clean it up
+
+ QDEL_LIST_ASSOC_VAL(tracking_atoms)
+
+ return ..()
+
+/// The proc to begin doing everything related to the tutorial
+/datum/tutorial/proc/start_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
+ template.load(bottom_left_corner_reservation, FALSE, TRUE)
+ var/obj/landmark = locate(/obj/effect/landmark/tutorial_bottom_left) in GLOB.landmarks_list
+ bottom_left_corner = get_turf(landmark)
+ qdel(landmark)
+
+ if(!verify_template_loaded())
+ abort_tutorial()
+ return FALSE
+
+ generate_binds()
+
+ GLOB.ongoing_tutorials |= src
+ 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)
+ for(var/datum/action/action in tutorial_mob.actions)
+ if(!istype(action, /datum/action/tutorial_end))
+ continue
+ action.Remove(tutorial_mob)
+ if(tutorial_mob.client?.prefs && completed)
+ tutorial_mob.client.prefs.completed_tutorials |= tutorial_id
+ tutorial_mob.client.prefs.save_character()
+ var/mob/dead/new_player/new_player = new
+ if(!tutorial_mob.mind)
+ tutorial_mob.mind_initialize()
+
+ tutorial_mob.mind.transfer_to(new_player)
+
+ if(!QDELETED(src))
+ qdel(src)
+
+/// Verify the template loaded fully and without error.
+/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(tile.type == /turf/closed)
+ return FALSE
+
+ return TRUE
+
+/// Something went very, very wrong during load so let's abort
+/datum/tutorial/proc/abort_tutorial()
+ to_chat(tutorial_mob, span_boldwarning("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)
+ tutorial_mob.play_screen_text(message, /atom/movable/screen/text/screen_text/tutorial, rgb(103, 214, 146))
+ to_chat(tutorial_mob, span_notice(message))
+
+/// Updates a player's objective in their status tab
+/datum/tutorial/proc/update_objective(message)
+ SEND_SIGNAL(tutorial_mob, COMSIG_MOB_TUTORIAL_UPDATE_OBJECTIVE, message)
+
+/// Initialize the tutorial mob.
+/datum/tutorial/proc/init_mob()
+ tutorial_mob.AddComponent(/datum/component/tutorial_status)
+ var/datum/action/tutorial_end/new_action = new /datum/action/tutorial_end()
+ new_action.Grant(tutorial_mob)
+ 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/new_player = new
+ if(!ghost.mind)
+ ghost.mind_initialize()
+
+ ghost.mind.transfer_to(new_player)
+
+ 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[action[1]] += bind
+ else
+ player_bind_dict[action[1]] = list(bind)
+
+/// 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"
+ button_icon_state = "hologram_exit"
+ /// 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/Trigger(trigger_flags)
+ 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/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/code/datums/tutorial/_tutorial_menu.dm b/code/datums/tutorial/_tutorial_menu.dm
new file mode 100644
index 00000000000..9d2b3323202
--- /dev/null
+++ b/code/datums/tutorial/_tutorial_menu.dm
@@ -0,0 +1,83 @@
+/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/misc/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
+ if(user.client?.prefs)
+ data["completed_tutorials"] = user.client.prefs.completed_tutorials
+ else
+ data["completed_tutorials"] = list()
+
+ 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/misc/tutorial))
+ to_chat(usr, span_notice("You are currently in a tutorial, or one is loading. Please be patient."))
+ return
+
+ path = new path
+ path.start_tutorial(usr)
+ return TRUE
+
+/mob/dead/new_player/proc/start_test_tutorial()
+ var/datum/tutorial/new_tutorial = new /datum/tutorial/vanderlin/injury
+ new_tutorial.start_tutorial(src)
diff --git a/code/datums/tutorial/area.dm b/code/datums/tutorial/area.dm
new file mode 100644
index 00000000000..1c1c6571bf5
--- /dev/null
+++ b/code/datums/tutorial/area.dm
@@ -0,0 +1,9 @@
+/area/misc/tutorial
+ name = "Tutorial Zone"
+ icon_state = "tutorial"
+ block_game_interaction = TRUE
+
+
+/// Marks the bottom left of the tutorial zone.
+/obj/effect/landmark/tutorial_bottom_left
+ name = "tutorial bottom left"
diff --git a/code/datums/tutorial/example_tutorial.dm b/code/datums/tutorial/example_tutorial.dm
new file mode 100644
index 00000000000..64705c9dbc5
--- /dev/null
+++ b/code/datums/tutorial/example_tutorial.dm
@@ -0,0 +1,68 @@
+/datum/tutorial/vanderlin/example
+ name = "Example Tutorial"
+ tutorial_id = "example" // This won't show up in the list, so this'll be irrelevant anyway.
+ category = TUTORIAL_CATEGORY_BASE
+ parent_path = /datum/tutorial/vanderlin/example
+
+// START OF SCRIPTING
+
+/datum/tutorial/vanderlin/example/start_tutorial(mob/starting_mob)
+ // Here, we're calling parent and checking its return value. If it has a falsey one (as done by !.), then something went wrong and we should abort
+ // There isn't really a reason that you _shouldn't_ have this
+ . = ..()
+ if(!.)
+ return
+
+ // Init_mob() isn't called by default, so we call it here
+ init_mob()
+ // As is standard, we give a message to the player and update their status panel with what we want done.
+ message_to_player("This is an example tutorial. Perform any emote to continue.")
+ update_objective("Do any emote.")
+ // This makes the player (tutorial_mob) listen for the COMSIG_MOB_EMOTE event, which will then call on_emote() when it hears it.
+ RegisterSignal(tutorial_mob, COMSIG_MOB_EMOTE, PROC_REF(on_emote))
+
+/datum/tutorial/vanderlin/example/proc/on_emote(datum/source)
+ // With any proc called via signal (see the RegisterSignal line above for details), we add SIGNAL_HANDLER to it.
+ SIGNAL_HANDLER
+
+ // Now that we've gotten the signal and started the script, we want to immediately stop listening for it.
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_EMOTE)
+ message_to_player("Good. Now, pick up that vial of Endurance Potion.")
+ update_objective("Pick up that can.")
+ // This macro takes a specific type path (the same used in init_map()) and a variable name to retrieve an object from the tracked object list
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/vial/endpot, beer_can)
+ // Now we're adding a yellow highlight around the can to make sure people know what we're talking about
+ add_highlight(beer_can)
+ // Now, we always prefer to register signals on the tutorial_mob (as opposed to the beer_can) whenever possible
+ RegisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_can_pickup))
+
+/// We get these arguments from the signal's definition. If you have VSC, ctrl+click on COMSIG_MOB_PICKUP_ITEM above. When dealing with a signal proc, `datum/source` is always the first argument, then any added ones
+/datum/tutorial/vanderlin/example/proc/on_can_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+
+ // Since we're just listening for the mob picking anything up, we want to confirm that the picked up item is the can before continuing. If it's not, then we return and keep listening.
+ if(!istype(picked_up, /obj/item/reagent_containers/glass/bottle/vial/endpot))
+ // If we hit this return here, then the picked up item wasn't the can, so we abort and keep listening.
+ return
+
+ // Since we passed the above if statement, stop listening for item pickups.
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM)
+ // Let's get the tracked beer can again.
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/vial/endpot, beer_can)
+ // And remove the highlight now that it's picked up
+ remove_highlight(beer_can)
+ message_to_player("Very good. This is the end of the example tutorial. You will be sent back to the lobby screen momentarily.")
+ // 7.5 seconds after the above message is sent, kick the player out and end the tutorial.
+ tutorial_end_in(7.5 SECONDS, TRUE)
+
+
+// END OF SCRIPTING
+// START OF SCRIPT HELPERS
+
+// END OF SCRIPT HELPERS
+
+/datum/tutorial/vanderlin/example/init_map()
+ // Here we're initializing a new can that we want to track, so we spawn it 2 tiles to the left and up from the bottom left corner of the tutorial zone
+ var/obj/item/reagent_containers/glass/bottle/vial/endpot/the_can = new(loc_from_corner(2, 2))
+ // Now we start tracking it
+ add_to_tracking_atoms(the_can)
diff --git a/code/datums/tutorial/make_a_tutorial.md b/code/datums/tutorial/make_a_tutorial.md
new file mode 100644
index 00000000000..96a7cb88682
--- /dev/null
+++ b/code/datums/tutorial/make_a_tutorial.md
@@ -0,0 +1,96 @@
+# Tutorial Creation
+
+[ToC]
+
+## Step 1: Identifying the Goal
+
+Your first objective when making a tutorial should be to have a clear and concise vision of what you want the tutorial to convey to the user. People absorb information better in smaller chunks, so you should ideally keep a tutorial to one section of information at a time.
+
+For example, if you are making a tutorial for new CM players, it should be split into multiple parts like:
+
+- Basics
+- Medical
+- Weaponry
+- Requisitions/Communication
+
+## Step 2: Coding
+
+For an example of the current code standards for tutorials, see [this](https://github.com/cmss13-devs/cmss13/pull/4442/files#diff-843b2f84360b9b932dfc960027992f2b5117667962bfa8da14f9a35f0179a926) file.
+
+The API for tutorials is designed to be very simple, so I'll go over all the base `/datum/tutorial` procs and some vars here:
+
+### Variables
+- `name`
+ - This is the player-facing name of the tutorial.
+- `tutorial_id`
+ - This is the back-end ID of the tutorial, used for save files. Try not to change a tutorial's ID after it's on the live server.
+- `category`
+ - This is what category the tutorial should be under. Use the `TUTORIAL_CATEGORY_XXXX` macros.
+- `tutorial_template`
+ - This is what type the map template of the tutorial should be. The default space is 12x12; ideally make it so it fits the given scale of the tutorial with some wiggle room for the player to move around.
+- `parent_path`
+ - This is the top-most parent `/datum/tutorial` path, used to exclude abstract parents from the tutorial menu. For example, `/datum/tutorial/marine/basic` would have a `parent_path` of `/datum/tutorial/marine`, since that path is the top-most abstract path.
+
+### Procs
+- `start_tutorial(mob/starting_mob)`
+ - This proc starts the tutorial, setting up the map template and player. This should be overridden with a parent call before any overridden code.
+- `end_tutorial(completed = FALSE)`
+ - This proc ends the tutorial, sending the player back to the lobby and deleting the tutorial itself. A parent call on any subtypes should be at the end of the overridden segment. If `completed` is `TRUE`, then the tutorial will save as a completed one for the user.
+- `add_highlight(atom/target, color = "#d19a02")`
+ - This proc adds a highlight filter around an atom, by default this color. Successive calls of highlight on the same atom will override the last.
+- `remove_highlight(atom/target)`
+ - This proc removes the tutorial highlight from a target.
+- `add_to_tracking_atoms(atom/reference)`
+ - This proc will add a reference to the tutorial's tracked atom dictionary. For what a tracked atom is, see Step 2.1.
+- `remove_from_tracking_atoms(atom/reference)`
+ - This proc will remove a reference from the tutorial's tracked atom dictionary. For what a tracked atom is, see Step 2.1.
+- `message_to_player(message)`
+ - This proc is the ideal way to communicate to a player. It is visually similar to overwatch messages or weather alerts, but appears and disappears much faster. The messages sent should be consise, but can have a degree of dialogue to them.
+- `update_objective(message)`
+ - This proc is used to update the player's objective in their status panel. This should be only what is required and how to do it without any dialogue or extra text.
+- `init_mob()`
+ - This proc is used to initialize the mob and set them up correctly.
+- `init_map()`
+ - This proc does nothing by default, but can be overriden to spawn any atoms necessary for the tutorial from the very start.
+- `tutorial_end_in(time = 5 SECONDS, completed = TRUE)`
+ - This proc will end the tutorial in the given time, defaulting to 5 seconds. Once the proc is called, the player will be booted back to the menu screen after the time is up. Will mark the tutorial as completed if `completed` is `TRUE`
+- `loc_from_corner(offset_x = 0, offset_y = 0)`
+ - This proc will return a turf offset from the bottom left corner of the tutorial zone. Keep in mind, the bottom left corner is NOT on a wall, it is on the first floor on the bottom left corner. `offset_x` and `offset_y` are used to offset what turf you want to get, and should never be negative.
+
+## Step 2.1: Tracking Atoms
+Naturally, you will need to keep track of certain objects or mobs for signal purposes, so the tracking system exists to fill that purpose. When you add a reference to the tracking atom list with `add_to_tracking_atoms()`, it gets put into a dictionary of `{path : reference}`. Because of this limitation, you should not track more than 1 object of the same type. To get a tracked atom, use of the `TUTORIAL_ATOM_FROM_TRACKING(path, varname)` macro is recommended. `path` should be replaced with the precise typepath of the tracked atom, and `varname` should be replaced with the variable name you wish to use. If an object is going to be deleted, remove it with `remove_from_tracking_atoms()` first.
+
+## Step 2.2: Scripting Format
+Any proc whose main purpose is to advance the tutorial will be hereon referred to as a "script proc", as part of the entire "script". In the vast majority of cases, a script proc should hand off to the next using signals. Here is an example from `basic_marine.dm`:
+
+```javascript
+/datum/tutorial/marine/basic/proc/on_cryopod_exit()
+ SIGNAL_HANDLER
+
+ UnregisterSignal(tracking_atoms[/obj/structure/machinery/cryopod/tutorial], COMSIG_CRYOPOD_GO_OUT)
+ message_to_player("Good. You may notice the yellow \"food\" icon on the right side of your screen. Proceed to the outlined Food Vendor and vend the USCM Protein Bar.")
+ update_objective("Vend a USCM Protein Bar from the outlined ColMarTech Food Vendor.")
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/structure/machinery/cm_vending/sorted/marine_food/tutorial, food_vendor)
+ add_highlight(food_vendor)
+ food_vendor.req_access = list()
+ RegisterSignal(food_vendor, COMSIG_VENDOR_SUCCESSFUL_VEND, PROC_REF(on_food_vend))
+
+```
+
+Line-by-line:
+ - `SIGNAL_HANDLER` is necessary as this proc was called via signal.
+ - Here we are unregistering the signal we registered in the previous proc to call this one, which in this case was waiting for the player to leave the tracked cryopod.
+ - Now, we tell the user the next step in the script, which is sent to their screen.
+ - Here we update the player's status panel with similar info to the above line, but far more condensed.
+ - Since we need to access the food vendor, we use the `TUTORIAL_ATOM_FROM_TRACKING()` macro to get a ref to it.
+ - We add a yellow outline to the food vendor to make it more clear what is wanted of the player
+ - The tutorial food vendors are locked to `ACCESS_TUTORIAL_LOCKED` by default, so here we remove that access requirement
+ - And finally, we register a signal for the next script proc, waiting for the user to vend something from the food vendor.
+
+
+## Step 2.3: Quirks & Tips
+- Generally speaking, you will want to create `/tutorial` subtypes of anything you add in the tutorial, should it need any special functions or similar.
+- Restrict access from players as much as possible. As seen in the example above, restricting access to vendors and similar machines is recommended to prevent sequence breaking. Additionally, avoid adding anything that detracts from the tutorial itself.
+- Attempt to avoid softlocks when possible. If someone could reasonably do something (e.g. firing every bullet they have at a ranged target and missing, now unable to kill them and progress) that could softlock them, then there should be a fallback of some sort. However, accomodations don't need to be made for people who purposefully cause a softlock; there's a "stop tutorial" button for a reason.
+- When calling `message_to_player()` or `update_objective()`, **bold** the names of objects, items, and keybinds.
+- Attempt to bind as many scripting signals to the `tutorial_mob` as possible. The nature of SS13 means something as sequence-heavy as this will always be fragile, so keeping the fragility we can affect to a minimum is imperative.
diff --git a/code/datums/tutorial/vanderlin/_base_vander_tutorial.dm b/code/datums/tutorial/vanderlin/_base_vander_tutorial.dm
new file mode 100644
index 00000000000..76c95ffa2e8
--- /dev/null
+++ b/code/datums/tutorial/vanderlin/_base_vander_tutorial.dm
@@ -0,0 +1,14 @@
+/datum/tutorial/vanderlin
+ category = TUTORIAL_CATEGORY_VANDERLIN
+ parent_path = /datum/tutorial/vanderlin
+ icon_state = "vanderlin"
+
+/datum/tutorial/vanderlin/init_mob()
+ var/mob/living/carbon/human/species/human/northern/new_character = new(bottom_left_corner)
+
+ new_character.key = tutorial_mob.key
+ tutorial_mob = new_character
+ //RegisterSignal(tutorial_mob, COMSIG_LIVING_GHOSTED, PROC_REF(on_ghost))
+ RegisterSignal(tutorial_mob, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH, COMSIG_MOB_END_TUTORIAL), PROC_REF(signal_end_tutorial))
+ RegisterSignal(tutorial_mob, COMSIG_MOB_LOGOUT, PROC_REF(on_logout))
+ return ..()
diff --git a/code/datums/tutorial/vanderlin/medical.dm b/code/datums/tutorial/vanderlin/medical.dm
new file mode 100644
index 00000000000..5a939f79166
--- /dev/null
+++ b/code/datums/tutorial/vanderlin/medical.dm
@@ -0,0 +1,193 @@
+/datum/tutorial/vanderlin/injury
+ name = "Treating Injuries"
+ tutorial_id = "injury_treatment"
+ category = TUTORIAL_CATEGORY_BASE
+ parent_path = /datum/tutorial/vanderlin/injury
+
+/datum/tutorial/vanderlin/injury/start_tutorial(mob/starting_mob)
+ . = ..()
+ if(!.)
+ return
+ init_mob()
+ message_to_player("You've taken some damage - a slash wound on your right arm and a burn on your head. \
+ Start by picking up the Ethanol Bottle.")
+ update_objective("Pick up the Ethanol Bottle.")
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/beer, ethanol_bottle)
+ add_highlight(ethanol_bottle)
+ RegisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_ethanol_pickup))
+
+/datum/tutorial/vanderlin/injury/proc/on_ethanol_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+ if(!istype(picked_up, /obj/item/reagent_containers/glass/bottle/beer))
+ return
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/beer, ethanol_bottle)
+ remove_highlight(ethanol_bottle)
+ message_to_player("Good. Now pick up the Cloth Bandage. You'll use it with Soak intent \
+ to absorb liquids from containers.")
+ update_objective("Pick up the Cloth Bandage.")
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/natural/cloth/bandage, cloth_bandage)
+ add_highlight(cloth_bandage)
+ RegisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_disinfect_bandage_pickup))
+
+/datum/tutorial/vanderlin/injury/proc/on_disinfect_bandage_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+ if(!istype(picked_up, /obj/item/natural/cloth/bandage))
+ return
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/natural/cloth/bandage, cloth_bandage)
+ remove_highlight(cloth_bandage)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/beer, ethanol_bottle)
+ add_highlight(ethanol_bottle)
+ message_to_player("Now soak the Cloth Bandage in the Ethanol Bottle. \
+ First, uncork the bottle by clicking it in your hand, then hold the bandage and click the bottle with Soak intent.")
+ update_objective("Soak the Cloth Bandage in the Ethanol Bottle.")
+ RegisterSignal(cloth_bandage, COMSIG_CLOTH_SOAKED, PROC_REF(on_bandage_soaked_ethanol))
+
+/datum/tutorial/vanderlin/injury/proc/on_bandage_soaked_ethanol(datum/source, atom/soaked_in)
+ SIGNAL_HANDLER
+ if(!istype(soaked_in, /obj/item/reagent_containers/glass/bottle/beer))
+ return
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/natural/cloth/bandage, cloth_bandage)
+ UnregisterSignal(cloth_bandage, COMSIG_CLOTH_SOAKED)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/beer, ethanol_bottle)
+ remove_highlight(ethanol_bottle)
+ message_to_player("Good. Now apply the soaked Cloth Bandage to your right arm to disinfect the slash wound. \
+ Make sure your right arm is selected in the zone selector.")
+ update_objective("Apply the soaked Cloth Bandage to your right arm.")
+ var/mob/living/carbon/human/mob = tutorial_mob
+ var/obj/item/bodypart/arm = mob.get_bodypart(BODY_ZONE_R_ARM)
+ RegisterSignal(arm, COMSIG_BODYPART_DISINFECTED, PROC_REF(on_arm_disinfected))
+
+/datum/tutorial/vanderlin/injury/proc/on_arm_disinfected(datum/source)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/mob = tutorial_mob
+ var/obj/item/bodypart/arm = mob.get_bodypart(BODY_ZONE_R_ARM)
+ UnregisterSignal(arm, COMSIG_BODYPART_DISINFECTED)
+ message_to_player("The wound is disinfected. Remove the bandage from your right arm before suturing - \
+ click your right arm with an empty hand to take it off.")
+ update_objective("Remove the bandage from your right arm.")
+ RegisterSignal(tutorial_mob, COMSIG_MOB_UNBANDAGE, PROC_REF(on_disinfect_bandage_removed))
+
+/datum/tutorial/vanderlin/injury/proc/on_disinfect_bandage_removed(datum/source, obj/item/bodypart/limb)
+ SIGNAL_HANDLER
+ // Removing the bandage from the limb puts it in the mob's hand, which fires after removal
+ var/mob/living/carbon/human/mob = tutorial_mob
+ var/obj/item/bodypart/arm = mob.get_bodypart(BODY_ZONE_R_ARM)
+ if(arm != limb)
+ return
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_UNBANDAGE)
+ message_to_player("Good. Now pick up the Suture Needle to stitch the slash closed.")
+ update_objective("Pick up the Suture Needle.")
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/needle, suture_needle)
+ add_highlight(suture_needle)
+ RegisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_needle_pickup))
+
+/datum/tutorial/vanderlin/injury/proc/on_needle_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+ if(!istype(picked_up, /obj/item/needle))
+ return
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/needle, suture_needle)
+ remove_highlight(suture_needle)
+ message_to_player("Now use the Suture Needle on your right arm to stitch the slash wound closed. \
+ Make sure your right arm is selected in the zone selector.")
+ update_objective("Use the Suture Needle on your right arm to suture the slash wound.")
+ var/mob/living/carbon/human/mob = tutorial_mob
+ var/obj/item/bodypart/arm = mob.get_bodypart(BODY_ZONE_R_ARM)
+ for(var/datum/injury/injury in arm.injuries)
+ if(injury.damage_type == WOUND_SLASH)
+ RegisterSignal(injury, COMSIG_INJURY_SUTURED, PROC_REF(on_slash_sutured))
+ break
+
+/datum/tutorial/vanderlin/injury/proc/on_slash_sutured(datum/source)
+ SIGNAL_HANDLER
+ UnregisterSignal(source, COMSIG_INJURY_SUTURED)
+ message_to_player("The slash is stitched. Now pick up the Calendula Salve Vial to treat the burn on your head.")
+ update_objective("Pick up the Calendula Salve Vial.")
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/vial/calendula_salve, salve_vial)
+ add_highlight(salve_vial)
+ RegisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_salve_pickup))
+
+/datum/tutorial/vanderlin/injury/proc/on_salve_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+ if(!istype(picked_up, /obj/item/reagent_containers/glass/bottle/vial/calendula_salve))
+ return
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/vial/calendula_salve, salve_vial)
+ remove_highlight(salve_vial)
+ message_to_player("Now pick up the Cloth Bandage to soak in the salve.")
+ update_objective("Pick up the Cloth Bandage.")
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/natural/cloth/bandage/tutorial_fresh, fresh_bandage)
+ add_highlight(fresh_bandage)
+ RegisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_salve_bandage_pickup))
+
+/datum/tutorial/vanderlin/injury/proc/on_salve_bandage_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+ if(!istype(picked_up, /obj/item/natural/cloth/bandage/tutorial_fresh))
+ return
+ UnregisterSignal(tutorial_mob, COMSIG_MOB_PICKUP_ITEM)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/natural/cloth/bandage/tutorial_fresh, fresh_bandage)
+ remove_highlight(fresh_bandage)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/vial/calendula_salve, salve_vial)
+ add_highlight(salve_vial)
+ message_to_player("Now soak the Cloth Bandage in the Calendula Salve Vial. \
+ First, uncork the vial by clicking it in your hand, then hold the bandage and click the vial with Soak intent.")
+ update_objective("Soak the Cloth Bandage in the Calendula Salve Vial.")
+ RegisterSignal(fresh_bandage, COMSIG_CLOTH_SOAKED, PROC_REF(on_bandage_soaked_salve))
+
+/datum/tutorial/vanderlin/injury/proc/on_bandage_soaked_salve(datum/source, atom/soaked_in)
+ SIGNAL_HANDLER
+ if(!istype(soaked_in, /obj/item/reagent_containers/glass/bottle/vial/calendula_salve))
+ return
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/natural/cloth/bandage/tutorial_fresh, fresh_bandage)
+ UnregisterSignal(fresh_bandage, COMSIG_CLOTH_SOAKED)
+ TUTORIAL_ATOM_FROM_TRACKING(/obj/item/reagent_containers/glass/bottle/vial/calendula_salve, salve_vial)
+ remove_highlight(salve_vial)
+ message_to_player("Good. Now apply the soaked Cloth Bandage to your head to treat the burn. \
+ Make sure your head is selected in the zone selector.")
+ update_objective("Apply the soaked Cloth Bandage to your head.")
+ var/mob/living/carbon/human/mob = tutorial_mob
+ var/obj/item/bodypart/head = mob.get_bodypart(BODY_ZONE_HEAD)
+ for(var/datum/injury/injury in head.injuries)
+ if(injury.damage_type == WOUND_BURN)
+ RegisterSignal(injury, COMSIG_INJURY_SALVED, PROC_REF(on_burn_salved))
+ break
+
+/datum/tutorial/vanderlin/injury/proc/on_burn_salved(datum/source)
+ SIGNAL_HANDLER
+ UnregisterSignal(source, COMSIG_INJURY_SALVED)
+ message_to_player("The burn is treated. Your wounds are closed and dressed - you're ready.")
+ tutorial_end_in(7.5 SECONDS, TRUE)
+
+/datum/tutorial/vanderlin/injury/init_map()
+ var/obj/item/reagent_containers/glass/bottle/beer/ethanol_bottle = new(loc_from_corner(2, 4))
+ add_to_tracking_atoms(ethanol_bottle)
+
+ var/obj/item/reagent_containers/glass/bottle/vial/calendula_salve/salve_vial = new(loc_from_corner(3, 4))
+ add_to_tracking_atoms(salve_vial)
+
+ // Soaked in salve and applied to the burn
+ var/obj/item/natural/cloth/bandage/cloth_bandage = new(loc_from_corner(4, 4))
+ add_to_tracking_atoms(cloth_bandage)
+
+ // Dry wrap for the final bandage step
+ var/obj/item/natural/cloth/bandage/tutorial_fresh/fresh_bandage = new(loc_from_corner(4, 3))
+ add_to_tracking_atoms(fresh_bandage)
+
+ var/obj/item/needle/suture_needle = new(loc_from_corner(5, 4))
+ add_to_tracking_atoms(suture_needle)
+
+/datum/tutorial/vanderlin/injury/init_mob()
+ . = ..()
+ var/mob/living/carbon/human/mob = tutorial_mob
+ var/obj/item/bodypart/arm = mob.get_bodypart(BODY_ZONE_R_ARM)
+ var/obj/item/bodypart/head = mob.get_bodypart(BODY_ZONE_HEAD)
+ if(!arm || !head)
+ return
+ var/datum/injury/slash = arm.create_injury(WOUND_SLASH, 25)
+ slash.adjust_germ_level(200)
+ head.create_injury(WOUND_BURN, 20)
+
+/obj/item/natural/cloth/bandage/tutorial_fresh
+ name = "cloth bandage" // Looks identical to the player
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 2ff523e69ed..214e3dd7d21 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -92,6 +92,8 @@
var/delver_restrictions = FALSE
var/coven_protected = FALSE
+ ///do we block certain interactions?
+ var/block_game_interaction = FALSE
/**
* A list of teleport locations
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index e54a7bd6c6a..49b6929a242 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -854,6 +854,7 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user)
+ SEND_SIGNAL(user, COMSIG_MOB_PICKUP_ITEM, src)
item_flags |= IN_INVENTORY
diff --git a/code/game/objects/items/natural/cloth.dm b/code/game/objects/items/natural/cloth.dm
index 0f7a27a3ba2..3899aaed58f 100644
--- a/code/game/objects/items/natural/cloth.dm
+++ b/code/game/objects/items/natural/cloth.dm
@@ -142,6 +142,7 @@
return
if(do_after(user, clean_speed, O))
O.reagents.trans_to(src, reagents.maximum_volume, 1, transfered_by = user)
+ SEND_SIGNAL(src, COMSIG_CLOTH_SOAKED, target)
user.visible_message(span_small("[user] soaks \the [src] in \the [O]."), span_small("I soak \the [src] in \the [O]."), vision_distance = 2)
playsound(O, pick('sound/foley/waterwash (1).ogg','sound/foley/waterwash (2).ogg'), 25, FALSE)
else if(isturf(target))
@@ -176,6 +177,7 @@
return
if(do_after(user, clean_speed * 2.5, O))
reagents.trans_to(O, reagents.total_volume, 1, transfered_by = user)
+ SEND_SIGNAL(src, COMSIG_CLOTH_SOAKED, target)
user.visible_message(span_small("[user] wrings out \the [src] in \the [O]."), span_small("I wring out \the [src] in \the [O]."), vision_distance = 2)
playsound(O, pick('sound/foley/waterwash (1).ogg','sound/foley/waterwash (2).ogg'), 25, FALSE)
else if(isturf(target))
diff --git a/code/modules/client/preferences/_preferences.dm b/code/modules/client/preferences/_preferences.dm
index db093973ce1..18e64de2a60 100644
--- a/code/modules/client/preferences/_preferences.dm
+++ b/code/modules/client/preferences/_preferences.dm
@@ -254,6 +254,9 @@ GLOBAL_LIST_INIT(name_adjustments, list())
/// culture datum type
var/datum/culture/culture = /datum/culture/universal/ambiguous
+ /// A list of tutorials that the client has completed, saved across rounds
+ var/list/completed_tutorials = list()
+
/datum/preferences/New(client/C)
parent = C
diff --git a/code/modules/client/preferences/preferences_savefile.dm b/code/modules/client/preferences/preferences_savefile.dm
index be8c4e4f014..2bdcf2daa4c 100644
--- a/code/modules/client/preferences/preferences_savefile.dm
+++ b/code/modules/client/preferences/preferences_savefile.dm
@@ -7,6 +7,22 @@
// where you would want the updater procs below to run
#define SAVEFILE_VERSION_MAX 33
+/// Converts a client's list of completed tutorials into a string for saving
+/datum/preferences/proc/tutorial_list_to_savestring()
+ if(!length(completed_tutorials))
+ return ""
+
+ var/return_string = ""
+ var/last_id = completed_tutorials[length(completed_tutorials)]
+ for(var/tutorial_id in completed_tutorials)
+ return_string += tutorial_id + (tutorial_id != last_id ? ";" : "")
+ return return_string
+
+/// Converts a saved string of completed tutorials into a list for in-game use
+/datum/preferences/proc/tutorial_savestring_to_list(savestring)
+ completed_tutorials = splittext(savestring, ";")
+ return completed_tutorials
+
/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
This proc checks if the current directory of the savefile S needs updating
@@ -183,6 +199,10 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
// Custom hotkeys
S["key_bindings"] >> key_bindings
+ var/tutorial_string = ""
+ S["completed_tutorials"] >> tutorial_string
+ tutorial_savestring_to_list(tutorial_string)
+
if(!char_theme)
char_theme = "grimshart"
//try to fix any outdated data if necessary
@@ -289,6 +309,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
WRITE_FILE(S["key_bindings"], key_bindings)
WRITE_FILE(S["multi_char_ready"], multi_char_ready)
WRITE_FILE(S["multi_ready_slots"], multi_ready_slots)
+ WRITE_FILE(S["completed_tutorials"], tutorial_list_to_savestring())
return TRUE
/datum/preferences/proc/_load_species(S)
diff --git a/code/modules/reagents/reagent_containers/bottles/alchemy.dm b/code/modules/reagents/reagent_containers/bottles/alchemy.dm
index 3c4fb2edafe..8e562273992 100644
--- a/code/modules/reagents/reagent_containers/bottles/alchemy.dm
+++ b/code/modules/reagents/reagent_containers/bottles/alchemy.dm
@@ -183,3 +183,6 @@
/obj/item/reagent_containers/glass/bottle/vial/mirrorwaste
list_reagents = list(/datum/reagent/poison/mirrorwaste = 25)
+
+/obj/item/reagent_containers/glass/bottle/vial/calendula_salve
+ list_reagents = list(/datum/reagent/medicine/herbal/calendula_salve = 25)
diff --git a/code/modules/screen_alerts/tutorial.dm b/code/modules/screen_alerts/tutorial.dm
new file mode 100644
index 00000000000..f5d4036a86c
--- /dev/null
+++ b/code/modules/screen_alerts/tutorial.dm
@@ -0,0 +1,6 @@
+/atom/movable/screen/text/screen_text/tutorial
+ letters_per_update = 4 // overall, pretty fast while not immediately popping in
+ play_delay = 0.1
+ fade_out_delay = 2.5 SECONDS
+ fade_out_time = 0.5 SECONDS
+ screen_loc = "CENTER-7,CENTER"
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 460e769af64..22d677290e8 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -1655,6 +1655,7 @@
injury.disinfect_injury()
if(time)
disinfects_in = addtimer(CALLBACK(src, PROC_REF(undisinfect_limb)), time, TIMER_STOPPABLE)
+ SEND_SIGNAL(src, COMSIG_BODYPART_DISINFECTED)
/obj/item/bodypart/proc/undisinfect_limb()
for(var/datum/injury/injury as anything in injuries)
@@ -1667,3 +1668,5 @@
/obj/item/bodypart/proc/unbandage_limb()
for(var/datum/injury/injury as anything in injuries)
injury.unbandage_injury()
+ if(owner)
+ SEND_SIGNAL(owner, COMSIG_MOB_UNBANDAGE, src)
diff --git a/code/modules/surgery/bodyparts/bodypart_wounds.dm b/code/modules/surgery/bodyparts/bodypart_wounds.dm
index 0961c7e6b4e..611d68b74f5 100644
--- a/code/modules/surgery/bodyparts/bodypart_wounds.dm
+++ b/code/modules/surgery/bodyparts/bodypart_wounds.dm
@@ -387,6 +387,7 @@
bandage = new_bandage
bandage_limb()
new_bandage.forceMove(src)
+ SEND_SIGNAL(src, COMSIG_BODYPART_BANDAGED, new_bandage)
return TRUE
/obj/item/bodypart/proc/try_bandage_expire()
diff --git a/vanderlin.dme b/vanderlin.dme
index b2ac9f4ed07..e4ecbd478a6 100644
--- a/vanderlin.dme
+++ b/vanderlin.dme
@@ -195,6 +195,7 @@
#include "code\__DEFINES\traders.dm"
#include "code\__DEFINES\triumphs.dm"
#include "code\__DEFINES\turfs.dm"
+#include "code\__DEFINES\tutorial.dm"
#include "code\__DEFINES\twitch.dm"
#include "code\__DEFINES\typeids.dm"
#include "code\__DEFINES\unit_tests.dm"
@@ -1077,6 +1078,7 @@
#include "code\datums\components\theme_song.dm"
#include "code\datums\components\tippable.dm"
#include "code\datums\components\tracking_beacon.dm"
+#include "code\datums\components\tutorial_status.dm"
#include "code\datums\components\twohanded.dm"
#include "code\datums\components\udder.dm"
#include "code\datums\components\use_mana.dm"
@@ -1653,6 +1655,12 @@
#include "code\datums\threat_regions\terror_bog.dm"
#include "code\datums\threat_regions\town.dm"
#include "code\datums\threat_regions\woods.dm"
+#include "code\datums\tutorial\_base_tutorial.dm"
+#include "code\datums\tutorial\_tutorial_menu.dm"
+#include "code\datums\tutorial\area.dm"
+#include "code\datums\tutorial\example_tutorial.dm"
+#include "code\datums\tutorial\vanderlin\_base_vander_tutorial.dm"
+#include "code\datums\tutorial\vanderlin\medical.dm"
#include "code\datums\world_factions\_base.dm"
#include "code\datums\world_factions\coastal.dm"
#include "code\datums\world_factions\mountain.dm"
@@ -3775,6 +3783,7 @@
#include "code\modules\relays\relays.dm"
#include "code\modules\screen_alerts\blurb.dm"
#include "code\modules\screen_alerts\screen_text.dm"
+#include "code\modules\screen_alerts\tutorial.dm"
#include "code\modules\spatial_grid\cell_tracking.dm"
#include "code\modules\spells\spell.dm"
#include "code\modules\spells\_casting\casting.dm"