Skip to content

Commit

Permalink
Tutorial documentation and proc update (#5921)
Browse files Browse the repository at this point in the history
# About the pull request
Updates tutorial documentation.
Adds a proc, `mark_completed()`, that makes the tutorial count as
completed regardless of if `end_tutorial()` was called with `TRUE` or
`FALSE` as an argument. Necessary for
#5909

---------

Co-authored-by: SabreML <[email protected]>
  • Loading branch information
Zonespace27 and SabreML authored Mar 13, 2024
1 parent 514007f commit 5794a71
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 57 deletions.
8 changes: 7 additions & 1 deletion code/datums/tutorial/_tutorial.dm
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ GLOBAL_LIST_EMPTY_TYPED(ongoing_tutorials, /datum/tutorial)
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()
/// If the tutorial has been completed. This doesn't need to be modified if you call end_tutorial() with a param of TRUE
var/completion_marked = FALSE

/datum/tutorial/Destroy(force, ...)
GLOB.ongoing_tutorials -= src
Expand Down Expand Up @@ -83,7 +85,7 @@ GLOBAL_LIST_EMPTY_TYPED(ongoing_tutorials, /datum/tutorial)

if(tutorial_mob)
remove_action(tutorial_mob, /datum/action/tutorial_end) // Just in case to make sure the client can't try and leave the tutorial while it's mid-cleanup
if(tutorial_mob.client?.prefs && completed)
if(tutorial_mob.client?.prefs && (completed || completion_marked))
tutorial_mob.client.prefs.completed_tutorials |= tutorial_id
tutorial_mob.client.prefs.save_character()
var/mob/new_player/new_player = new
Expand Down Expand Up @@ -210,6 +212,10 @@ GLOBAL_LIST_EMPTY_TYPED(ongoing_tutorials, /datum/tutorial)

return player_bind_dict[action_name][1]

/// When called, will make anything that ends the tutorial mark it as completed. Does not need to be called if end_tutorial(TRUE) is called instead
/datum/tutorial/proc/mark_completed()
completion_marked = TRUE

/datum/action/tutorial_end
name = "Stop Tutorial"
action_icon_state = "hologram_exit"
Expand Down
128 changes: 72 additions & 56 deletions code/datums/tutorial/creating_a_tutorial.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# 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
- Basics
- Medical
- Weaponry
- Requisitions/Communication

## Step 2: Coding

Expand All @@ -20,47 +18,65 @@ For an example of the current code standards for tutorials, see [this](https://g
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<string>`
- This is the player-facing name of the tutorial.
- `tutorial_id<string>`
- 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<string>`
- This is what category the tutorial should be under. Use the `TUTORIAL_CATEGORY_XXXX` macros.
- `tutorial_template</datum/map_template/tutorial>`
- 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<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.

- `name<string>`
- This is the player-facing name of the tutorial.
- `tutorial_id<string>`
- 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<string>`
- This is what category the tutorial should be under. Use the `TUTORIAL_CATEGORY_XXXX` macros.
- `tutorial_template</datum/map_template/tutorial>`
- 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<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.
- `completion_marked<bool>`
- If this is `TRUE`, the tutorial will be marked as completed if ended in any way. You can modify this with `mark_completed()` but is not necessary if `end_tutorial(TRUE)` is called.

### 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 <span style="color:#d19a02">this</span> 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.

- `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. If `mark_completed()` was called previously, the tutorial will count as completed regardless of if this is called with an argument of `TRUE` or `FALSE`.
- `add_highlight(atom/target, color = "#d19a02")`
- This proc adds a highlight filter around an atom, by default <span style="color:#d19a02">this</span> 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.
- `on_ghost(datum/source, mob/dead/observer/ghost)`
- This proc is used to properly end and clean up the tutorial should a player ghost out. You shouldn't need to override or modify this when making a tutorial.
- `signal_end_tutorial(datum/source)`
- This proc is used to call `end_tutorial()` via signals. If something (e.g. a player dying) should send a signal that ends the tutorial, have the signal call this proc.
- `on_logout(datum/source)`
- This proc is called when a player logs out, disconnecting their client from the server. As with `on_ghost()` and similar procs, it cleans up and ends the tutorial.
- `generate_binds()`
- This proc generates a dictionary of the player's keybinds, in the form of {"action_name" : "key_to_press"}. This is used for the `retrieve_bind()` proc to be able to tell the user what buttons to press.
- `retrieve_bind(action_name)`
- This proc will be one you'll get a fair amount of use from. Whenever you tell the user to do something like "drop an item", you should tell them what button to press by calling `retrieve_bind("drop_item")` in the string telling them to drop an item.
- `mark_completed()`
- This proc can be used as an alternative to calling `end_tutorial(TRUE)`. Calling this proc means any method of exiting the tutorial (ghosting, dying, pressing the exit button) will mark the tutorial as completed.

## 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
Expand All @@ -69,28 +85,28 @@ Any proc whose main purpose is to advance the tutorial will be hereon referred t

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 <b>Food Vendor</b> and vend the <b>USCM Protein Bar</b>.")
update_objective("Vend a <b>USCM Protein Bar</b> from the outlined <b>ColMarTech Food Vendor</b>.")
TUTORIAL_ATOM_FROM_TRACKING(/obj/structure/machinery/cm_vending/sorted/marine_food/tutorial, food_vendor)
update_objective("Vend a <b>USCM Protein Bar</b> from the outlined <b>ColMarTech Food Vendor</b>.")
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.

- `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 inthis 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 morecondensed.
- Since we need to access the food vendor, we use the `TUTORIAL_ATOM_FROM_TRACKING()` macro to get aref 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 thataccess 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.

- 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.

0 comments on commit 5794a71

Please sign in to comment.