mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 10:11:09 +00:00
Contextual tutorials for swapping hands and dropping items (#72292)
# Requires https://github.com/tgstation/tgstation/pull/72320 ## About The Pull Request https://user-images.githubusercontent.com/35135081/209700892-e54be6cf-d18c-4d12-acd1-e5eb46e9d82d.mp4 https://user-images.githubusercontent.com/35135081/209700911-751b8a0e-d770-49fa-a6eb-ce50aa0fa670.mp4 --- Adds a system for tutorials that: - Are contextually given - Are not given again after completion - Can optionally not trigger for anyone who first played before a certain date Uses this system for a tutorial for switching hands/dropping items. This tutorial is triggered when you try to click on an item with another item, and `afterattack` return FALSE. In order for this to work as smoothly as possible, I'm going to open a separate PR that cleans up the `afterattack` on everything to either return TRUE/FALSE. ## Why It's Good For The Game SS13 is an extremely confusing game, being able to do tutorials in a non-intrusive way (like a separate tutorial mode) is nice. The system in place is going to be perfectly usable for introducing mechanics to both fresh players and experienced players alike (such as for future content). ## Changelog 🆑 qol: New players will now get a contextual tutorial for how to switch hands and drop items. /🆑 Co-authored-by: Fikou <23585223+Fikou@users.noreply.github.com>
This commit is contained in:
@@ -2,14 +2,28 @@ Any time you make a change to the schema files, remember to increment the databa
|
|||||||
|
|
||||||
Make sure to also update `DB_MAJOR_VERSION` and `DB_MINOR_VERSION`, which can be found in `code/__DEFINES/subsystem.dm`.
|
Make sure to also update `DB_MAJOR_VERSION` and `DB_MINOR_VERSION`, which can be found in `code/__DEFINES/subsystem.dm`.
|
||||||
|
|
||||||
The latest database version is 5.21; The query to update the schema revision table is:
|
The latest database version is 5.23; The query to update the schema revision table is:
|
||||||
|
|
||||||
INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 22);
|
INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 23);
|
||||||
or
|
or
|
||||||
INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 22);
|
INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 23);
|
||||||
|
|
||||||
In any query remember to add a prefix to the table names if you use one.
|
In any query remember to add a prefix to the table names if you use one.
|
||||||
|
|
||||||
|
-----------------------------------------------------
|
||||||
|
Version 5.23, 28 December 2022, by Mothblocks
|
||||||
|
Added `tutorial_completions` to mark what ckeys have completed contextual tutorials.
|
||||||
|
|
||||||
|
```
|
||||||
|
CREATE TABLE `tutorial_completions` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`ckey` VARCHAR(32) NOT NULL,
|
||||||
|
`tutorial_key` VARCHAR(64) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `ckey_tutorial_unique` (`ckey`, `tutorial_key`));
|
||||||
|
```
|
||||||
|
|
||||||
|
-----------------------------------------------------
|
||||||
Version 5.22, 22 December 2021, by Mothblocks
|
Version 5.22, 22 December 2021, by Mothblocks
|
||||||
Fixes a bug in `telemetry_connections` that limited the range of IPs.
|
Fixes a bug in `telemetry_connections` that limited the range of IPs.
|
||||||
|
|
||||||
|
|||||||
@@ -693,6 +693,14 @@ CREATE TABLE `telemetry_connections` (
|
|||||||
UNIQUE INDEX `unique_constraints` (`ckey` , `telemetry_ckey` , `address` , `computer_id`)
|
UNIQUE INDEX `unique_constraints` (`ckey` , `telemetry_ckey` , `address` , `computer_id`)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `tutorial_completions`;
|
||||||
|
CREATE TABLE `tutorial_completions` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`ckey` VARCHAR(32) NOT NULL,
|
||||||
|
`tutorial_key` VARCHAR(64) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `ckey_tutorial_unique` (`ckey`, `tutorial_key`));
|
||||||
|
|
||||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
|||||||
@@ -693,6 +693,14 @@ CREATE TABLE `SS13_telemetry_connections` (
|
|||||||
UNIQUE INDEX `unique_constraints` (`ckey` , `telemetry_ckey` , `address` , `computer_id`)
|
UNIQUE INDEX `unique_constraints` (`ckey` , `telemetry_ckey` , `address` , `computer_id`)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `SS13_tutorial_completions`;
|
||||||
|
CREATE TABLE `SS13_tutorial_completions` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`ckey` VARCHAR(32) NOT NULL,
|
||||||
|
`tutorial_key` VARCHAR(64) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `ckey_tutorial_unique` (`ckey`, `tutorial_key`));
|
||||||
|
|
||||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#define COLOR_ALMOST_BLACK "#333333"
|
#define COLOR_ALMOST_BLACK "#333333"
|
||||||
#define COLOR_FULL_TONER_BLACK "#101010"
|
#define COLOR_FULL_TONER_BLACK "#101010"
|
||||||
#define COLOR_PRISONER_BLACK "#292929"
|
#define COLOR_PRISONER_BLACK "#292929"
|
||||||
|
#define COLOR_NEARLY_ALL_BLACK "#111111"
|
||||||
#define COLOR_BLACK "#000000"
|
#define COLOR_BLACK "#000000"
|
||||||
#define COLOR_HALF_TRANSPARENT_BLACK "#0000007A"
|
#define COLOR_HALF_TRANSPARENT_BLACK "#0000007A"
|
||||||
|
|
||||||
|
|||||||
@@ -133,5 +133,9 @@
|
|||||||
|
|
||||||
/// From /mob/living/befriend() : (mob/living/new_friend)
|
/// From /mob/living/befriend() : (mob/living/new_friend)
|
||||||
#define COMSIG_LIVING_BEFRIENDED "living_befriended"
|
#define COMSIG_LIVING_BEFRIENDED "living_befriended"
|
||||||
|
|
||||||
|
/// From /obj/item/proc/pickup(): (/obj/item/picked_up_item)
|
||||||
|
#define COMSIG_LIVING_PICKED_UP_ITEM "living_picked_up_item"
|
||||||
|
|
||||||
/// From /mob/living/unfriend() : (mob/living/old_friend)
|
/// From /mob/living/unfriend() : (mob/living/old_friend)
|
||||||
#define COMSIG_LIVING_UNFRIENDED "living_unfriended"
|
#define COMSIG_LIVING_UNFRIENDED "living_unfriended"
|
||||||
|
|||||||
@@ -116,9 +116,12 @@
|
|||||||
#define MOB_DEADSAY_SIGNAL_INTERCEPT (1<<0)
|
#define MOB_DEADSAY_SIGNAL_INTERCEPT (1<<0)
|
||||||
///from /mob/living/emote(): ()
|
///from /mob/living/emote(): ()
|
||||||
#define COMSIG_MOB_EMOTE "mob_emote"
|
#define COMSIG_MOB_EMOTE "mob_emote"
|
||||||
///from base of mob/swap_hand(): (obj/item)
|
///from base of mob/swap_hand(): (obj/item/currently_held_item)
|
||||||
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
|
#define COMSIG_MOB_SWAPPING_HANDS "mob_swapping_hands"
|
||||||
#define COMPONENT_BLOCK_SWAP (1<<0)
|
#define COMPONENT_BLOCK_SWAP (1<<0)
|
||||||
|
/// from base of mob/swap_hand(): ()
|
||||||
|
/// Performed after the hands are swapped.
|
||||||
|
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
|
||||||
///from base of /mob/verb/pointed: (atom/A)
|
///from base of /mob/verb/pointed: (atom/A)
|
||||||
#define COMSIG_MOB_POINTED "mob_pointed"
|
#define COMSIG_MOB_POINTED "mob_pointed"
|
||||||
///Mob is trying to open the wires of a target [/atom], from /datum/wires/interactable(): (atom/target)
|
///Mob is trying to open the wires of a target [/atom], from /datum/wires/interactable(): (atom/target)
|
||||||
@@ -171,5 +174,9 @@
|
|||||||
|
|
||||||
///from living/flash_act(), when a mob is successfully flashed.
|
///from living/flash_act(), when a mob is successfully flashed.
|
||||||
#define COMSIG_MOB_FLASHED "mob_flashed"
|
#define COMSIG_MOB_FLASHED "mob_flashed"
|
||||||
|
|
||||||
/// from mob/get_status_tab_items(): (list/items)
|
/// from mob/get_status_tab_items(): (list/items)
|
||||||
#define COMSIG_MOB_GET_STATUS_TAB_ITEMS "mob_get_status_tab_items"
|
#define COMSIG_MOB_GET_STATUS_TAB_ITEMS "mob_get_status_tab_items"
|
||||||
|
|
||||||
|
/// from mob/proc/dropItemToGround()
|
||||||
|
#define COMSIG_MOB_DROPPING_ITEM "mob_dropping_item"
|
||||||
|
|||||||
@@ -240,6 +240,9 @@
|
|||||||
///Layer for screentips
|
///Layer for screentips
|
||||||
#define SCREENTIP_LAYER 4
|
#define SCREENTIP_LAYER 4
|
||||||
|
|
||||||
|
/// Layer for tutorial instructions
|
||||||
|
#define TUTORIAL_INSTRUCTIONS_LAYER 5
|
||||||
|
|
||||||
|
|
||||||
#define LOBBY_BACKGROUND_LAYER 3
|
#define LOBBY_BACKGROUND_LAYER 3
|
||||||
#define LOBBY_BUTTON_LAYER 4
|
#define LOBBY_BUTTON_LAYER 4
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
/// Helper macro for compile time translation
|
/// Helper macro for creating a matrix at the given offsets.
|
||||||
|
/// Works at compile time.
|
||||||
#define TRANSLATE_MATRIX(offset_x, offset_y) matrix(1, 0, (offset_x), 0, 1, (offset_y))
|
#define TRANSLATE_MATRIX(offset_x, offset_y) matrix(1, 0, (offset_x), 0, 1, (offset_y))
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
*
|
*
|
||||||
* make sure you add an update to the schema_version stable in the db changelog
|
* make sure you add an update to the schema_version stable in the db changelog
|
||||||
*/
|
*/
|
||||||
#define DB_MINOR_VERSION 22
|
#define DB_MINOR_VERSION 23
|
||||||
|
|
||||||
|
|
||||||
//! ## Timing subsystem
|
//! ## Timing subsystem
|
||||||
|
|||||||
@@ -18,14 +18,26 @@
|
|||||||
y += view_size[2]
|
y += view_size[2]
|
||||||
if(findtext(screen_loc, "SOUTH"))
|
if(findtext(screen_loc, "SOUTH"))
|
||||||
y += world.icon_size
|
y += world.icon_size
|
||||||
// Cut out everything we just parsed
|
|
||||||
screen_loc = cut_relative_direction(screen_loc)
|
|
||||||
|
|
||||||
var/list/x_and_y = splittext(screen_loc, ",")
|
var/list/x_and_y = splittext(screen_loc, ",")
|
||||||
|
|
||||||
var/list/x_pack = splittext(x_and_y[1], ":")
|
var/list/x_pack = splittext(x_and_y[1], ":")
|
||||||
var/list/y_pack = splittext(x_and_y[2], ":")
|
var/list/y_pack = splittext(x_and_y[2], ":")
|
||||||
x += text2num(x_pack[1]) * world.icon_size
|
|
||||||
y += text2num(y_pack[1]) * world.icon_size
|
var/x_coord = x_pack[1]
|
||||||
|
var/y_coord = y_pack[1]
|
||||||
|
|
||||||
|
if (findtext(x_coord, "CENTER"))
|
||||||
|
x += view_size[1] / 2
|
||||||
|
|
||||||
|
if (findtext(y_coord, "CENTER"))
|
||||||
|
y += view_size[2] / 2
|
||||||
|
|
||||||
|
x_coord = text2num(cut_relative_direction(x_coord))
|
||||||
|
y_coord = text2num(cut_relative_direction(y_coord))
|
||||||
|
|
||||||
|
x += x_coord * world.icon_size
|
||||||
|
y += y_coord * world.icon_size
|
||||||
|
|
||||||
if(length(x_pack) > 1)
|
if(length(x_pack) > 1)
|
||||||
x += text2num(x_pack[2])
|
x += text2num(x_pack[2])
|
||||||
|
|||||||
@@ -59,6 +59,14 @@
|
|||||||
if (after_attack_secondary_result == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN || after_attack_secondary_result == SECONDARY_ATTACK_CONTINUE_CHAIN)
|
if (after_attack_secondary_result == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN || after_attack_secondary_result == SECONDARY_ATTACK_CONTINUE_CHAIN)
|
||||||
return TRUE
|
return TRUE
|
||||||
|
|
||||||
|
var/afterattack_result = afterattack(target, user, TRUE, params)
|
||||||
|
|
||||||
|
if (!(afterattack_result & AFTERATTACK_PROCESSED_ITEM) && isitem(target))
|
||||||
|
if (isnull(user.get_inactive_held_item()))
|
||||||
|
SStutorials.suggest_tutorial(user, /datum/tutorial/switch_hands, params2list(params))
|
||||||
|
else
|
||||||
|
SStutorials.suggest_tutorial(user, /datum/tutorial/drop, params2list(params))
|
||||||
|
|
||||||
return afterattack(target, user, TRUE, params) == TRUE
|
return afterattack(target, user, TRUE, params) == TRUE
|
||||||
|
|
||||||
/// Called when the item is in the active hand, and clicked; alternately, there is an 'activate held object' verb or you can hit pagedown.
|
/// Called when the item is in the active hand, and clicked; alternately, there is an 'activate held object' verb or you can hit pagedown.
|
||||||
|
|||||||
@@ -414,3 +414,4 @@
|
|||||||
|
|
||||||
/datum/config_entry/flag/disallow_circuit_sounds
|
/datum/config_entry/flag/disallow_circuit_sounds
|
||||||
|
|
||||||
|
/datum/config_entry/flag/give_tutorials_without_db
|
||||||
|
|||||||
111
code/controllers/subsystem/tutorials.dm
Normal file
111
code/controllers/subsystem/tutorials.dm
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/// Namespace for housing code relating to giving contextual tutorials to users.
|
||||||
|
SUBSYSTEM_DEF(tutorials)
|
||||||
|
name = "Tutorials"
|
||||||
|
flags = SS_NO_FIRE
|
||||||
|
|
||||||
|
/// A mapping of /datum/tutorial type to their manager singleton.
|
||||||
|
/// You probably shouldn't be indexing this directly.
|
||||||
|
var/list/datum/tutorial_manager/tutorial_managers = list()
|
||||||
|
|
||||||
|
VAR_PRIVATE/list/datum/tutorial_manager/tutorial_managers_by_key = list()
|
||||||
|
|
||||||
|
/datum/controller/subsystem/tutorials/Initialize()
|
||||||
|
init_tutorial_managers()
|
||||||
|
load_initial_tutorial_completions()
|
||||||
|
RegisterSignal(SSdcs, COMSIG_GLOB_CLIENT_CONNECT, PROC_REF(on_client_connect))
|
||||||
|
|
||||||
|
return SS_INIT_SUCCESS
|
||||||
|
|
||||||
|
/// Will suggest the passed tutorial type to the user.
|
||||||
|
/// Will check that they should actually see it, e.g. hasn't completed it yet, etc.
|
||||||
|
/// Then, calls `/datum/tutorial/subtype/perform` with the extra arguments passed in.
|
||||||
|
/datum/controller/subsystem/tutorials/proc/suggest_tutorial(mob/user, datum/tutorial/tutorial_type, ...)
|
||||||
|
var/datum/tutorial_manager/tutorial_manager = tutorial_managers[tutorial_type]
|
||||||
|
if (isnull(tutorial_manager))
|
||||||
|
CRASH("[tutorial_type] is not a valid tutorial type")
|
||||||
|
|
||||||
|
if (!tutorial_manager.should_run(user))
|
||||||
|
return
|
||||||
|
|
||||||
|
INVOKE_ASYNC(tutorial_manager, TYPE_PROC_REF(/datum/tutorial_manager, try_perform), user, args.Copy(3))
|
||||||
|
|
||||||
|
/datum/controller/subsystem/tutorials/proc/init_tutorial_managers()
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
|
||||||
|
for (var/datum/tutorial/tutorial_type as anything in subtypesof(/datum/tutorial))
|
||||||
|
var/datum/tutorial_manager/tutorial_manager = new /datum/tutorial_manager(tutorial_type)
|
||||||
|
tutorial_managers[tutorial_type] = tutorial_manager
|
||||||
|
tutorial_managers_by_key[tutorial_manager.get_key()] = tutorial_manager
|
||||||
|
|
||||||
|
/datum/controller/subsystem/tutorials/proc/load_initial_tutorial_completions()
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
set waitfor = FALSE // There's no reason to halt init for this
|
||||||
|
|
||||||
|
var/list/ckey_options = list()
|
||||||
|
var/list/ckeys = list()
|
||||||
|
|
||||||
|
for (var/client/client as anything in GLOB.clients)
|
||||||
|
var/ckey = client?.ckey
|
||||||
|
if (!ckey)
|
||||||
|
continue // client shenanigans, never trust
|
||||||
|
|
||||||
|
var/index = ckeys.len + 1
|
||||||
|
ckey_options += ":ckey[index]"
|
||||||
|
ckeys["ckey[index]"] = ckey
|
||||||
|
|
||||||
|
if (ckey_options.len == 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
var/datum/db_query/select_all_query = SSdbcore.NewQuery(
|
||||||
|
"SELECT ckey, tutorial_key FROM [format_table_name("tutorial_completions")] WHERE ckey in ([ckey_options.Join(", ")])",
|
||||||
|
ckeys,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!select_all_query.Execute())
|
||||||
|
qdel(select_all_query)
|
||||||
|
return
|
||||||
|
|
||||||
|
while (select_all_query.NextRow())
|
||||||
|
var/ckey = select_all_query.item[1]
|
||||||
|
var/tutorial_key = select_all_query.item[2]
|
||||||
|
|
||||||
|
mark_ckey_completed_tutorial(ckey, tutorial_key)
|
||||||
|
|
||||||
|
qdel(select_all_query)
|
||||||
|
|
||||||
|
/datum/controller/subsystem/tutorials/proc/on_client_connect(datum/source, client/client)
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
var/ckey = client.ckey
|
||||||
|
if (!ckey)
|
||||||
|
return
|
||||||
|
|
||||||
|
INVOKE_ASYNC(src, PROC_REF(check_completed_tutorials_for_ckey), ckey)
|
||||||
|
|
||||||
|
/datum/controller/subsystem/tutorials/proc/check_completed_tutorials_for_ckey(ckey)
|
||||||
|
if (!SSdbcore.IsConnected())
|
||||||
|
return
|
||||||
|
|
||||||
|
var/datum/db_query/select_tutorials_for_ckey = SSdbcore.NewQuery(
|
||||||
|
"SELECT tutorial_key FROM [format_table_name("tutorial_completions")] WHERE ckey = :ckey",
|
||||||
|
list("ckey" = ckey)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!select_tutorials_for_ckey.Execute())
|
||||||
|
return
|
||||||
|
|
||||||
|
while (select_tutorials_for_ckey.NextRow())
|
||||||
|
var/tutorial_key = select_tutorials_for_ckey.item[1]
|
||||||
|
|
||||||
|
mark_ckey_completed_tutorial(ckey, tutorial_key)
|
||||||
|
|
||||||
|
qdel(select_tutorials_for_ckey)
|
||||||
|
|
||||||
|
/datum/controller/subsystem/tutorials/proc/mark_ckey_completed_tutorial(ckey, tutorial_key)
|
||||||
|
var/datum/tutorial_manager/tutorial_manager = tutorial_managers_by_key[tutorial_key]
|
||||||
|
if (isnull(tutorial_manager))
|
||||||
|
// Not necessarily a bug.
|
||||||
|
// Could be an outdated server or a removed tutorial.
|
||||||
|
return
|
||||||
|
|
||||||
|
tutorial_manager.mark_as_completed(ckey)
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
return // blocked wield from item
|
return // blocked wield from item
|
||||||
wielded = TRUE
|
wielded = TRUE
|
||||||
ADD_TRAIT(parent, TRAIT_WIELDED, REF(src))
|
ADD_TRAIT(parent, TRAIT_WIELDED, REF(src))
|
||||||
RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, PROC_REF(on_swap_hands))
|
RegisterSignal(user, COMSIG_MOB_SWAPPING_HANDS, PROC_REF(on_swapping_hands))
|
||||||
wield_callback?.Invoke(parent, user)
|
wield_callback?.Invoke(parent, user)
|
||||||
|
|
||||||
// update item stats and name
|
// update item stats and name
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
/**
|
/**
|
||||||
* on_swap_hands Triggers on swapping hands, blocks swap if the other hand is busy
|
* on_swap_hands Triggers on swapping hands, blocks swap if the other hand is busy
|
||||||
*/
|
*/
|
||||||
/datum/component/two_handed/proc/on_swap_hands(mob/user, obj/item/held_item)
|
/datum/component/two_handed/proc/on_swapping_hands(mob/user, obj/item/held_item)
|
||||||
SIGNAL_HANDLER
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
if(!held_item)
|
if(!held_item)
|
||||||
|
|||||||
@@ -658,6 +658,7 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
|
|||||||
/obj/item/proc/pickup(mob/user)
|
/obj/item/proc/pickup(mob/user)
|
||||||
SHOULD_CALL_PARENT(TRUE)
|
SHOULD_CALL_PARENT(TRUE)
|
||||||
SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user)
|
SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user)
|
||||||
|
SEND_SIGNAL(user, COMSIG_LIVING_PICKED_UP_ITEM, src)
|
||||||
item_flags |= IN_INVENTORY
|
item_flags |= IN_INVENTORY
|
||||||
|
|
||||||
/// called when "found" in pockets and storage items. Returns 1 if the search should end.
|
/// called when "found" in pockets and storage items. Returns 1 if the search should end.
|
||||||
|
|||||||
@@ -280,9 +280,15 @@
|
|||||||
* If the item can be dropped, it will be forceMove()'d to the ground and the turf's Entered() will be called.
|
* If the item can be dropped, it will be forceMove()'d to the ground and the turf's Entered() will be called.
|
||||||
*/
|
*/
|
||||||
/mob/proc/dropItemToGround(obj/item/I, force = FALSE, silent = FALSE, invdrop = TRUE)
|
/mob/proc/dropItemToGround(obj/item/I, force = FALSE, silent = FALSE, invdrop = TRUE)
|
||||||
|
if (isnull(I))
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
SEND_SIGNAL(src, COMSIG_MOB_DROPPING_ITEM)
|
||||||
. = doUnEquip(I, force, drop_location(), FALSE, invdrop = invdrop, silent = silent)
|
. = doUnEquip(I, force, drop_location(), FALSE, invdrop = invdrop, silent = silent)
|
||||||
|
|
||||||
if(!. || !I) //ensure the item exists and that it was dropped properly.
|
if(!. || !I) //ensure the item exists and that it was dropped properly.
|
||||||
return
|
return
|
||||||
|
|
||||||
if(!(I.item_flags & NO_PIXEL_RANDOM_DROP))
|
if(!(I.item_flags & NO_PIXEL_RANDOM_DROP))
|
||||||
I.pixel_x = I.base_pixel_x + rand(-6, 6)
|
I.pixel_x = I.base_pixel_x + rand(-6, 6)
|
||||||
I.pixel_y = I.base_pixel_y + rand(-6, 6)
|
I.pixel_y = I.base_pixel_y + rand(-6, 6)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
QDEL_NULL(dna)
|
QDEL_NULL(dna)
|
||||||
GLOB.carbon_list -= src
|
GLOB.carbon_list -= src
|
||||||
|
|
||||||
/mob/living/carbon/swap_hand(held_index)
|
/mob/living/carbon/perform_hand_swap(held_index)
|
||||||
. = ..()
|
. = ..()
|
||||||
if(!.)
|
if(!.)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -397,7 +397,7 @@
|
|||||||
if(slot_num > 4) // not >3 otherwise cycling with just one item on module 3 wouldn't work
|
if(slot_num > 4) // not >3 otherwise cycling with just one item on module 3 wouldn't work
|
||||||
slot_num = 1 //Wrap around.
|
slot_num = 1 //Wrap around.
|
||||||
|
|
||||||
/mob/living/silicon/robot/swap_hand()
|
/mob/living/silicon/robot/perform_hand_swap()
|
||||||
cycle_modules()
|
cycle_modules()
|
||||||
|
|
||||||
/mob/living/silicon/robot/can_hold_items(obj/item/I)
|
/mob/living/silicon/robot/can_hold_items(obj/item/I)
|
||||||
|
|||||||
@@ -616,7 +616,7 @@
|
|||||||
else
|
else
|
||||||
mode()
|
mode()
|
||||||
|
|
||||||
/mob/living/simple_animal/swap_hand(hand_index)
|
/mob/living/simple_animal/perform_hand_swap(hand_index)
|
||||||
. = ..()
|
. = ..()
|
||||||
if(!.)
|
if(!.)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -862,11 +862,23 @@
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
/mob/proc/swap_hand()
|
/mob/proc/swap_hand(held_index)
|
||||||
|
SHOULD_NOT_OVERRIDE(TRUE) // Override perform_hand_swap instead
|
||||||
|
|
||||||
var/obj/item/held_item = get_active_held_item()
|
var/obj/item/held_item = get_active_held_item()
|
||||||
if(SEND_SIGNAL(src, COMSIG_MOB_SWAP_HANDS, held_item) & COMPONENT_BLOCK_SWAP)
|
if(SEND_SIGNAL(src, COMSIG_MOB_SWAPPING_HANDS, held_item) & COMPONENT_BLOCK_SWAP)
|
||||||
to_chat(src, span_warning("Your other hand is too busy holding [held_item]."))
|
to_chat(src, span_warning("Your other hand is too busy holding [held_item]."))
|
||||||
return FALSE
|
return FALSE
|
||||||
|
|
||||||
|
var/result = perform_hand_swap(held_index)
|
||||||
|
if (result)
|
||||||
|
SEND_SIGNAL(src, COMSIG_MOB_SWAP_HANDS)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
/// Performs the actual ritual of swapping hands, such as setting the held index variables
|
||||||
|
/mob/proc/perform_hand_swap(held_index)
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
return TRUE
|
return TRUE
|
||||||
|
|
||||||
/mob/proc/activate_hand(selhand)
|
/mob/proc/activate_hand(selhand)
|
||||||
|
|||||||
257
code/modules/tutorials/_tutorial.dm
Normal file
257
code/modules/tutorials/_tutorial.dm
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/// The base for a contextual tutorial.
|
||||||
|
/// In order to give a tutorial to someone, use `SStutorials.suggest_tutorial(user, /datum/tutorial/subtype)`
|
||||||
|
/datum/tutorial
|
||||||
|
/// If set, any account who started playing before this date will not be given this tutorial.
|
||||||
|
/// Date is in YYYY-MM-DD format.
|
||||||
|
var/grandfather_date
|
||||||
|
|
||||||
|
/// The mob we are giving the tutorial to
|
||||||
|
VAR_PROTECTED/mob/user
|
||||||
|
|
||||||
|
VAR_PRIVATE/atom/movable/screen/tutorial_instruction/instruction_screen
|
||||||
|
|
||||||
|
/datum/tutorial/New(mob/user)
|
||||||
|
src.user = user
|
||||||
|
|
||||||
|
RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(destroy_self))
|
||||||
|
RegisterSignal(user.client, COMSIG_PARENT_QDELETING, PROC_REF(destroy_self))
|
||||||
|
|
||||||
|
/datum/tutorial/Destroy(force, ...)
|
||||||
|
user.client?.screen -= instruction_screen
|
||||||
|
QDEL_NULL(instruction_screen)
|
||||||
|
|
||||||
|
user = null
|
||||||
|
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/// Gets the [`/datum/tutorial_manager`] that owns this tutorial.
|
||||||
|
/datum/tutorial/proc/manager()
|
||||||
|
RETURN_TYPE(/datum/tutorial_manager)
|
||||||
|
return SStutorials.tutorial_managers[type]
|
||||||
|
|
||||||
|
/// The actual steps of the tutorial. Is given any excess arguments of suggest_tutorial.
|
||||||
|
/// Must be overridden.
|
||||||
|
/datum/tutorial/proc/perform()
|
||||||
|
SHOULD_CALL_PARENT(FALSE)
|
||||||
|
CRASH("[type] does not override perform()")
|
||||||
|
|
||||||
|
/// Returns TRUE/FALSE if this tutorial should be given.
|
||||||
|
/// If FALSE, does not mean it won't come back later.
|
||||||
|
/datum/tutorial/proc/should_perform()
|
||||||
|
SHOULD_CALL_PARENT(FALSE)
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
/// Called by the tutorial when the user has successfully completed it.
|
||||||
|
/// Will mark it as completed in the datbaase and kick off destruction of the tutorial.
|
||||||
|
/datum/tutorial/proc/complete()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
|
SHOULD_NOT_OVERRIDE(TRUE)
|
||||||
|
|
||||||
|
manager().complete(user)
|
||||||
|
perform_base_completion_effects()
|
||||||
|
|
||||||
|
/// As opposed to `complete()`, this merely hides the tutorial.
|
||||||
|
/// This should be used when the user doesn't need the tutorial anymore, but didn't
|
||||||
|
/// actually properly finish it.
|
||||||
|
/datum/tutorial/proc/dismiss()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
|
SHOULD_NOT_OVERRIDE(TRUE)
|
||||||
|
|
||||||
|
manager().dismiss(user)
|
||||||
|
perform_base_completion_effects()
|
||||||
|
|
||||||
|
#define INSTRUCTION_SCREEN_DELAY (1 SECONDS)
|
||||||
|
|
||||||
|
/datum/tutorial/proc/perform_base_completion_effects()
|
||||||
|
SHOULD_NOT_OVERRIDE(TRUE)
|
||||||
|
|
||||||
|
var/delay = perform_completion_effects_with_delay()
|
||||||
|
|
||||||
|
if (!isnull(instruction_screen))
|
||||||
|
animate(instruction_screen, time = INSTRUCTION_SCREEN_DELAY, alpha = 0, easing = SINE_EASING)
|
||||||
|
delay += INSTRUCTION_SCREEN_DELAY
|
||||||
|
|
||||||
|
QDEL_IN(src, delay)
|
||||||
|
|
||||||
|
/// Called when the tutorial is being hidden, but before it is deleted.
|
||||||
|
/// You should unregister signals and fade out any of your creations in here.
|
||||||
|
/// Returns how long extra to delay the deletion.
|
||||||
|
/datum/tutorial/proc/perform_completion_effects_with_delay()
|
||||||
|
SHOULD_CALL_PARENT(FALSE)
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
#undef INSTRUCTION_SCREEN_DELAY
|
||||||
|
|
||||||
|
/datum/tutorial/proc/destroy_self()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
SHOULD_NOT_OVERRIDE(TRUE)
|
||||||
|
|
||||||
|
manager().dismiss(user)
|
||||||
|
qdel(src)
|
||||||
|
|
||||||
|
/// Shows a large piece of text on the user's screen with the given message.
|
||||||
|
/// If a message already exists, will fade it out and replace it.
|
||||||
|
/datum/tutorial/proc/show_instruction(message)
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
|
|
||||||
|
if (isnull(instruction_screen))
|
||||||
|
instruction_screen = new(null, message, user.client)
|
||||||
|
user.client?.screen += instruction_screen
|
||||||
|
else
|
||||||
|
instruction_screen.change_message(message)
|
||||||
|
|
||||||
|
/// Given a keybind and a message, will replace %KEY% in `message` with the first keybind they have.
|
||||||
|
/// As a fallback, will return the third parameter, `message_without_keybinds`, if none are set.
|
||||||
|
/datum/tutorial/proc/keybinding_message(datum/keybinding/keybinding_type, message, message_without_keybinds)
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
|
|
||||||
|
var/list/keybinds = user.client?.prefs.key_bindings[initial(keybinding_type.name)]
|
||||||
|
return keybinds?.len > 0 ? replacetext(message, "%KEY%", "<b>[keybinds[1]]</b>") : message_without_keybinds
|
||||||
|
|
||||||
|
/// Creates a UI element with the given `icon_state`, starts it at `initial_screen_loc`, and animates it to `target_screen_loc`.
|
||||||
|
/// Waits `animate_start_time` before moving.
|
||||||
|
/datum/tutorial/proc/animate_ui_element(icon_state, initial_screen_loc, target_screen_loc, animate_start_time)
|
||||||
|
PROTECTED_PROC(TRUE)
|
||||||
|
|
||||||
|
var/atom/movable/screen/preview = new
|
||||||
|
preview.icon = ui_style2icon(user.client?.prefs.read_preference(/datum/preference/choiced/ui_style) || GLOB.available_ui_styles[1])
|
||||||
|
preview.icon_state = icon_state
|
||||||
|
preview.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
||||||
|
preview.screen_loc = "1,1"
|
||||||
|
|
||||||
|
var/view = user.client?.view
|
||||||
|
|
||||||
|
var/list/origin_offsets = screen_loc_to_offset(initial_screen_loc, view)
|
||||||
|
|
||||||
|
// A little offset to the right
|
||||||
|
var/matrix/origin_transform = TRANSLATE_MATRIX(origin_offsets[1] - world.icon_size * 0.5, origin_offsets[2] - world.icon_size * 1.5)
|
||||||
|
|
||||||
|
var/list/target_offsets = screen_loc_to_offset(target_screen_loc, view)
|
||||||
|
// `- world.icon_Size * 0.5` to patch over a likely bug in screen_loc_to_offset with CENTER, needs more looking at
|
||||||
|
var/matrix/animate_to_transform = TRANSLATE_MATRIX(target_offsets[1] - world.icon_size * 1.5, target_offsets[2] - world.icon_size)
|
||||||
|
|
||||||
|
preview.transform = origin_transform
|
||||||
|
|
||||||
|
preview.alpha = 0
|
||||||
|
animate(preview, time = animate_start_time, alpha = 255, easing = CUBIC_EASING)
|
||||||
|
animate(1.4 SECONDS)
|
||||||
|
animate(transform = animate_to_transform, time = 2 SECONDS, easing = SINE_EASING | EASE_IN)
|
||||||
|
animate(alpha = 0, time = 2.4 SECONDS, easing = CUBIC_EASING | EASE_IN, flags = ANIMATION_PARALLEL)
|
||||||
|
|
||||||
|
user.client?.screen += preview
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
/// A singleton that manages when to create tutorials of a specific tutorial type.
|
||||||
|
/datum/tutorial_manager
|
||||||
|
VAR_PRIVATE/datum/tutorial/tutorial_type
|
||||||
|
|
||||||
|
/// ckeys that we know have finished the tutorial
|
||||||
|
VAR_PRIVATE/list/finished_ckeys = list()
|
||||||
|
|
||||||
|
/// ckeys that have performed the tutorial, but have not completed it.
|
||||||
|
/// Doesn't mean that they can still see the tutorial, might have meant the tutorial was dismissed
|
||||||
|
/// without being completed, such as during a log out.
|
||||||
|
VAR_PRIVATE/list/performing_ckeys = list()
|
||||||
|
|
||||||
|
/datum/tutorial_manager/New(tutorial_type)
|
||||||
|
ASSERT(ispath(tutorial_type, /datum/tutorial))
|
||||||
|
src.tutorial_type = tutorial_type
|
||||||
|
|
||||||
|
/datum/tutorial_manager/Destroy(force, ...)
|
||||||
|
if (!force)
|
||||||
|
stack_trace("Something is trying to destroy [type], which is a singleton")
|
||||||
|
return QDEL_HINT_LETMELIVE
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/// Checks if we should perform the tutorial for the given user, and performs if so.
|
||||||
|
/// Use `SStutorials.suggest_tutorial` instead of calling this directly.
|
||||||
|
/datum/tutorial_manager/proc/try_perform(mob/user, list/arguments)
|
||||||
|
var/datum/tutorial/tutorial = new tutorial_type(user)
|
||||||
|
if (!tutorial.should_perform(user))
|
||||||
|
qdel(tutorial)
|
||||||
|
return
|
||||||
|
|
||||||
|
performing_ckeys[user.ckey] = TRUE
|
||||||
|
|
||||||
|
tutorial.perform(arglist(arguments))
|
||||||
|
|
||||||
|
/// Checks if the user should be given this tutorial
|
||||||
|
/datum/tutorial_manager/proc/should_run(mob/user)
|
||||||
|
var/ckey = user.ckey
|
||||||
|
|
||||||
|
if (isnull(ckey))
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
if (ckey in finished_ckeys)
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
if (ckey in performing_ckeys)
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
if (!SSdbcore.IsConnected())
|
||||||
|
return CONFIG_GET(flag/give_tutorials_without_db)
|
||||||
|
|
||||||
|
var/player_join_date = user.client?.player_join_date
|
||||||
|
if (isnull(player_join_date))
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
// This works because ISO-8601 is cool
|
||||||
|
var/grandfather_date = initial(tutorial_type.grandfather_date)
|
||||||
|
if (!isnull(grandfather_date) && player_join_date < grandfather_date)
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
/// Marks the tutorial as completed.
|
||||||
|
/// Call `/datum/tutorial/proc/complete()` instead.
|
||||||
|
/datum/tutorial_manager/proc/complete(mob/user)
|
||||||
|
set waitfor = FALSE
|
||||||
|
|
||||||
|
ASSERT(!isnull(user.ckey))
|
||||||
|
|
||||||
|
finished_ckeys[user.ckey] = TRUE
|
||||||
|
performing_ckeys -= user.ckey
|
||||||
|
|
||||||
|
SSblackbox.record_feedback("tally", "tutorial_completed", 1, "[tutorial_type]")
|
||||||
|
log_game("[key_name(user)] completed the [tutorial_type] tutorial.")
|
||||||
|
|
||||||
|
if (SSdbcore.IsConnected())
|
||||||
|
INVOKE_ASYNC(src, PROC_REF(log_completion_to_database), user.ckey)
|
||||||
|
|
||||||
|
/datum/tutorial_manager/proc/log_completion_to_database(ckey)
|
||||||
|
PRIVATE_PROC(TRUE)
|
||||||
|
|
||||||
|
var/datum/db_query/insert_tutorial_query = SSdbcore.NewQuery(
|
||||||
|
"INSERT INTO [format_table_name("tutorial_completions")] (ckey, tutorial_key) VALUES (:ckey, :tutorial_key) ON DUPLICATE KEY UPDATE tutorial_key = tutorial_key",
|
||||||
|
list(
|
||||||
|
"ckey" = ckey,
|
||||||
|
"tutorial_key" = get_key(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
insert_tutorial_query.warn_execute()
|
||||||
|
|
||||||
|
qdel(insert_tutorial_query)
|
||||||
|
|
||||||
|
/// Dismisses the tutorial, not marking it as completed in the database.
|
||||||
|
/// Call `/datum/tutorial/proc/dismiss()` instead.
|
||||||
|
/datum/tutorial_manager/proc/dismiss(mob/user)
|
||||||
|
performing_ckeys -= user.ckey
|
||||||
|
|
||||||
|
/// Given a ckey, will mark them as being completed without affecting the database.
|
||||||
|
/// Call `/datum/tutorial/proc/complete()` instead.
|
||||||
|
/datum/tutorial_manager/proc/mark_as_completed(ckey)
|
||||||
|
finished_ckeys[ckey] = TRUE
|
||||||
|
performing_ckeys -= ckey
|
||||||
|
|
||||||
|
/// Gives the key that will be saved in the database.
|
||||||
|
/// Must be 64 characters or less.
|
||||||
|
/datum/tutorial_manager/proc/get_key()
|
||||||
|
SHOULD_BE_PURE(TRUE)
|
||||||
|
return copytext("[tutorial_type]", length("[/datum/tutorial]") + 2)
|
||||||
53
code/modules/tutorials/tutorial_instruction.dm
Normal file
53
code/modules/tutorials/tutorial_instruction.dm
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/atom/movable/screen/tutorial_instruction
|
||||||
|
icon = 'icons/effects/alphacolors.dmi'
|
||||||
|
icon_state = "white"
|
||||||
|
color = COLOR_NEARLY_ALL_BLACK
|
||||||
|
alpha = 0
|
||||||
|
screen_loc = "TOP-2,CENTER"
|
||||||
|
layer = TUTORIAL_INSTRUCTIONS_LAYER
|
||||||
|
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
||||||
|
|
||||||
|
var/client/client
|
||||||
|
var/atom/movable/screen/tutorial_instruction_text/instruction_text
|
||||||
|
|
||||||
|
/atom/movable/screen/tutorial_instruction/Initialize(mapload, message, client/client)
|
||||||
|
. = ..()
|
||||||
|
|
||||||
|
transform = transform.Scale(36, 2.5)
|
||||||
|
|
||||||
|
src.client = client
|
||||||
|
animate(src, alpha = 245, time = 0.8 SECONDS, easing = SINE_EASING)
|
||||||
|
|
||||||
|
instruction_text = new(src, message, client)
|
||||||
|
vis_contents += instruction_text
|
||||||
|
|
||||||
|
/atom/movable/screen/tutorial_instruction/Destroy()
|
||||||
|
client = null
|
||||||
|
QDEL_NULL(instruction_text)
|
||||||
|
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/atom/movable/screen/tutorial_instruction/proc/change_message(message)
|
||||||
|
instruction_text.change_message(message)
|
||||||
|
|
||||||
|
/atom/movable/screen/tutorial_instruction_text
|
||||||
|
maptext_height = 480
|
||||||
|
maptext_y = -2
|
||||||
|
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
||||||
|
layer = TUTORIAL_INSTRUCTIONS_LAYER
|
||||||
|
|
||||||
|
/atom/movable/screen/tutorial_instruction_text/Initialize(mapload, message, client/client)
|
||||||
|
. = ..()
|
||||||
|
|
||||||
|
var/view = client?.view_size.getView()
|
||||||
|
maptext_width = view ? view_to_pixels(view)[1] : 480
|
||||||
|
pixel_x = (maptext_width - world.icon_size) * -0.5
|
||||||
|
|
||||||
|
change_message(message)
|
||||||
|
|
||||||
|
/atom/movable/screen/tutorial_instruction_text/proc/change_message(message)
|
||||||
|
// We don't use MAPTEXT macro here because it doesn't handle big text
|
||||||
|
message = "<span style='font-family: \"VCR OSD Mono\"; font-size: 22px; text-align: center'>[message]</span>"
|
||||||
|
|
||||||
|
animate(src, alpha = 0, time = (maptext ? 0.5 SECONDS : 0), easing = SINE_EASING)
|
||||||
|
animate(alpha = 255, time = 0.5 SECONDS, maptext = message)
|
||||||
109
code/modules/tutorials/tutorials/drop.dm
Normal file
109
code/modules/tutorials/tutorials/drop.dm
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#define TIME_TO_START_MOVING_DROP_ICON (0.5 SECONDS)
|
||||||
|
|
||||||
|
#define STAGE_DROP_ITEM "STAGE_DROP_ITEM"
|
||||||
|
#define STAGE_PICK_SOMETHING_UP "STAGE_PICK_SOMETHING_UP"
|
||||||
|
|
||||||
|
/// Tutorial for showing how to drop items.
|
||||||
|
/// Fired when clicking on an item with another item with a filled inactive hand.
|
||||||
|
/datum/tutorial/drop
|
||||||
|
grandfather_date = "2023-01-07"
|
||||||
|
|
||||||
|
var/stage = STAGE_DROP_ITEM
|
||||||
|
var/atom/movable/screen/drop_preview
|
||||||
|
var/obj/last_held_item
|
||||||
|
|
||||||
|
/datum/tutorial/drop/Destroy(force, ...)
|
||||||
|
last_held_item = null
|
||||||
|
user.client?.screen -= drop_preview
|
||||||
|
QDEL_NULL(drop_preview)
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/datum/tutorial/drop/perform(list/params)
|
||||||
|
create_drop_preview(params[SCREEN_LOC])
|
||||||
|
addtimer(CALLBACK(src, PROC_REF(show_instructions)), TIME_TO_START_MOVING_DROP_ICON)
|
||||||
|
|
||||||
|
RegisterSignal(user, COMSIG_MOB_DROPPING_ITEM, PROC_REF(on_dropped_item))
|
||||||
|
RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, PROC_REF(on_swap_hands))
|
||||||
|
RegisterSignal(user, COMSIG_LIVING_PICKED_UP_ITEM, PROC_REF(on_pick_up_item))
|
||||||
|
|
||||||
|
update_held_item()
|
||||||
|
|
||||||
|
/datum/tutorial/drop/perform_completion_effects_with_delay()
|
||||||
|
UnregisterSignal(user, list(COMSIG_MOB_DROPPING_ITEM, COMSIG_MOB_SWAP_HANDS, COMSIG_LIVING_PICKED_UP_ITEM))
|
||||||
|
if (!isnull(last_held_item))
|
||||||
|
UnregisterSignal(last_held_item, COMSIG_MOVABLE_MOVED)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
/datum/tutorial/drop/proc/create_drop_preview(initial_screen_loc)
|
||||||
|
drop_preview = animate_ui_element(
|
||||||
|
"act_drop",
|
||||||
|
initial_screen_loc,
|
||||||
|
ui_drop_throw,
|
||||||
|
TIME_TO_START_MOVING_DROP_ICON,
|
||||||
|
)
|
||||||
|
|
||||||
|
/datum/tutorial/drop/proc/show_instructions()
|
||||||
|
if (QDELETED(src))
|
||||||
|
return
|
||||||
|
|
||||||
|
switch (stage)
|
||||||
|
if (STAGE_DROP_ITEM)
|
||||||
|
show_instruction(keybinding_message(
|
||||||
|
/datum/keybinding/mob/drop_item,
|
||||||
|
"Press '%KEY%' to drop your current item",
|
||||||
|
"Click '<b>DROP</b>' to drop your current item",
|
||||||
|
))
|
||||||
|
if (STAGE_PICK_SOMETHING_UP)
|
||||||
|
show_instruction("Pick something up!")
|
||||||
|
|
||||||
|
/datum/tutorial/drop/proc/on_swap_hands()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
if (isnull(user.get_active_held_item()))
|
||||||
|
if (stage != STAGE_PICK_SOMETHING_UP)
|
||||||
|
stage = STAGE_PICK_SOMETHING_UP
|
||||||
|
show_instructions()
|
||||||
|
else if (stage == STAGE_PICK_SOMETHING_UP)
|
||||||
|
stage = STAGE_DROP_ITEM
|
||||||
|
show_instructions()
|
||||||
|
|
||||||
|
update_held_item()
|
||||||
|
|
||||||
|
/datum/tutorial/drop/proc/on_dropped_item()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
stage = STAGE_PICK_SOMETHING_UP
|
||||||
|
show_instructions()
|
||||||
|
|
||||||
|
/datum/tutorial/drop/proc/on_pick_up_item()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
if (stage != STAGE_PICK_SOMETHING_UP)
|
||||||
|
dismiss()
|
||||||
|
return
|
||||||
|
|
||||||
|
complete()
|
||||||
|
|
||||||
|
// Exists so that if we, say, place the item on a table, we don't count that as completion
|
||||||
|
/datum/tutorial/drop/proc/update_held_item()
|
||||||
|
if (!isnull(last_held_item))
|
||||||
|
UnregisterSignal(last_held_item, COMSIG_MOVABLE_MOVED)
|
||||||
|
|
||||||
|
last_held_item = user.get_active_held_item()
|
||||||
|
if (isnull(last_held_item))
|
||||||
|
return
|
||||||
|
|
||||||
|
RegisterSignal(last_held_item, COMSIG_MOVABLE_MOVED, PROC_REF(on_held_item_moved))
|
||||||
|
|
||||||
|
/datum/tutorial/drop/proc/on_held_item_moved()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
if (stage == STAGE_PICK_SOMETHING_UP)
|
||||||
|
return
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
|
||||||
|
#undef STAGE_DROP_ITEM
|
||||||
|
#undef STAGE_PICK_SOMETHING_UP
|
||||||
|
#undef TIME_TO_START_MOVING_DROP_ICON
|
||||||
86
code/modules/tutorials/tutorials/switch_hands.dm
Normal file
86
code/modules/tutorials/tutorials/switch_hands.dm
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#define TIME_TO_START_MOVING_HAND_ICON (0.5 SECONDS)
|
||||||
|
|
||||||
|
#define STAGE_SHOULD_SWAP_HAND "STAGE_SHOULD_SWAP_HAND"
|
||||||
|
#define STAGE_PICK_UP_ITEM "STAGE_PICK_UP_ITEM"
|
||||||
|
|
||||||
|
/// Tutorial for showing how to switch hands.
|
||||||
|
/// Fired when clicking on an item with another item with an empty inactive hand.
|
||||||
|
/datum/tutorial/switch_hands
|
||||||
|
grandfather_date = "2023-01-07"
|
||||||
|
|
||||||
|
var/stage = STAGE_SHOULD_SWAP_HAND
|
||||||
|
var/atom/movable/screen/hand_preview
|
||||||
|
|
||||||
|
// So that they don't just drop the item
|
||||||
|
var/hand_to_watch
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/New(mob/user)
|
||||||
|
. = ..()
|
||||||
|
|
||||||
|
hand_to_watch = (user.active_hand_index % user.held_items.len) + 1
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/Destroy(force, ...)
|
||||||
|
user.client?.screen -= hand_preview
|
||||||
|
QDEL_NULL(hand_preview)
|
||||||
|
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/perform(list/params)
|
||||||
|
create_hand_preview(params[SCREEN_LOC])
|
||||||
|
addtimer(CALLBACK(src, PROC_REF(show_instructions)), TIME_TO_START_MOVING_HAND_ICON)
|
||||||
|
|
||||||
|
RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, PROC_REF(on_swap_hands))
|
||||||
|
RegisterSignal(user, COMSIG_LIVING_PICKED_UP_ITEM, PROC_REF(on_pick_up_item))
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/perform_completion_effects_with_delay()
|
||||||
|
UnregisterSignal(user, list(COMSIG_MOB_SWAP_HANDS, COMSIG_LIVING_PICKED_UP_ITEM))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/proc/create_hand_preview(initial_screen_loc)
|
||||||
|
hand_preview = animate_ui_element(
|
||||||
|
"hand_[hand_to_watch % 2 == 0 ? "r" : "l"]",
|
||||||
|
initial_screen_loc,
|
||||||
|
ui_hand_position(hand_to_watch),
|
||||||
|
TIME_TO_START_MOVING_HAND_ICON,
|
||||||
|
)
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/proc/show_instructions()
|
||||||
|
if (QDELETED(src))
|
||||||
|
return
|
||||||
|
|
||||||
|
switch (stage)
|
||||||
|
if (STAGE_SHOULD_SWAP_HAND)
|
||||||
|
var/hand_name = hand_to_watch % 2 == 0 ? "right" : "left"
|
||||||
|
show_instruction(keybinding_message(
|
||||||
|
/datum/keybinding/mob/swap_hands,
|
||||||
|
"Press '%KEY%' to use your [hand_name] hand",
|
||||||
|
"Click '<b>SWAP</b>' to use your [hand_name] hand",
|
||||||
|
))
|
||||||
|
if (STAGE_PICK_UP_ITEM)
|
||||||
|
show_instruction("Pick something up!")
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/proc/on_swap_hands()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
if (isnull(user.get_active_held_item()))
|
||||||
|
stage = STAGE_PICK_UP_ITEM
|
||||||
|
show_instructions()
|
||||||
|
else if (isnull(user.get_inactive_held_item()))
|
||||||
|
stage = STAGE_SHOULD_SWAP_HAND
|
||||||
|
show_instructions()
|
||||||
|
else
|
||||||
|
// You somehow got an item in both hands during the tutorial without switching hands.
|
||||||
|
// Good job I guess?
|
||||||
|
complete()
|
||||||
|
|
||||||
|
/datum/tutorial/switch_hands/proc/on_pick_up_item()
|
||||||
|
SIGNAL_HANDLER
|
||||||
|
|
||||||
|
if (user.active_hand_index != hand_to_watch)
|
||||||
|
return
|
||||||
|
|
||||||
|
complete()
|
||||||
|
|
||||||
|
#undef STAGE_PICK_UP_ITEM
|
||||||
|
#undef STAGE_SHOULD_SWAP_HAND
|
||||||
|
#undef TIME_TO_START_MOVING_HAND_ICON
|
||||||
@@ -205,6 +205,7 @@
|
|||||||
#include "tgui_create_message.dm"
|
#include "tgui_create_message.dm"
|
||||||
#include "timer_sanity.dm"
|
#include "timer_sanity.dm"
|
||||||
#include "traitor.dm"
|
#include "traitor.dm"
|
||||||
|
#include "tutorial_sanity.dm"
|
||||||
#include "unit_test.dm"
|
#include "unit_test.dm"
|
||||||
#include "verify_config_tags.dm"
|
#include "verify_config_tags.dm"
|
||||||
#include "verify_emoji_names.dm"
|
#include "verify_emoji_names.dm"
|
||||||
|
|||||||
19
code/modules/unit_tests/tutorial_sanity.dm
Normal file
19
code/modules/unit_tests/tutorial_sanity.dm
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/// Verifies that every tutorial has properly set variables
|
||||||
|
/datum/unit_test/tutorial_sanity
|
||||||
|
|
||||||
|
/datum/unit_test/tutorial_sanity/Run()
|
||||||
|
var/regex/regex_valid_date = regex(@"\d{4}-\d{2}-\d{2}")
|
||||||
|
var/list/keys = list()
|
||||||
|
|
||||||
|
for (var/datum/tutorial/tutorial_type as anything in SStutorials.tutorial_managers)
|
||||||
|
var/datum/tutorial_manager/tutorial_manager = SStutorials.tutorial_managers[tutorial_type]
|
||||||
|
|
||||||
|
var/grandfather_date = initial(tutorial_type.grandfather_date)
|
||||||
|
if (!isnull(grandfather_date))
|
||||||
|
TEST_ASSERT(regex_valid_date.Find(grandfather_date), "[tutorial_type] has an invalid grandfather_date ([grandfather_date])")
|
||||||
|
|
||||||
|
var/key = tutorial_manager.get_key()
|
||||||
|
TEST_ASSERT(!(key in keys), "[key] shows up twice")
|
||||||
|
TEST_ASSERT(length(key) < 64, "[key] is more than 64 characters, it won't fit in the SQL table.")
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SStutorials.tutorial_managers.len, length(subtypesof(/datum/tutorial)), "Expected tutorial_managers to have one of every tutorial")
|
||||||
@@ -522,3 +522,8 @@ MAXFINE 2000
|
|||||||
## Comment if you wish to enable title music playing at the lobby screen. This flag is disabled by default to facilitate better code testing on local machines.
|
## Comment if you wish to enable title music playing at the lobby screen. This flag is disabled by default to facilitate better code testing on local machines.
|
||||||
## Do keep in mind that this flag will not affect individual player's preferences: if they opt-out on your server, it will never play for them.
|
## Do keep in mind that this flag will not affect individual player's preferences: if they opt-out on your server, it will never play for them.
|
||||||
DISALLOW_TITLE_MUSIC
|
DISALLOW_TITLE_MUSIC
|
||||||
|
|
||||||
|
## If enabled, then when the database is disabled, all players will get tutorials.
|
||||||
|
## This is primarily useful for developing tutorials. If you have a proper DB setup, you
|
||||||
|
## don't need (or want) this.
|
||||||
|
#GIVE_TUTORIALS_WITHOUT_DB
|
||||||
|
|||||||
BIN
interface/VCR_OSD_Mono.ttf
Normal file
BIN
interface/VCR_OSD_Mono.ttf
Normal file
Binary file not shown.
10
interface/fonts.dm
Normal file
10
interface/fonts.dm
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/// A font datum, it exists to define a custom font to use in a span style later.
|
||||||
|
/datum/font
|
||||||
|
/// Font name, just so people know what to put in their span style.
|
||||||
|
var/name
|
||||||
|
/// The font file we link to.
|
||||||
|
var/font_family
|
||||||
|
|
||||||
|
/datum/font/vcr_osd_mono
|
||||||
|
name = "VCR OSD Mono"
|
||||||
|
font_family = 'interface/VCR_OSD_Mono.ttf'
|
||||||
2
interface/license.txt
Normal file
2
interface/license.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VCR OSD Mono created by Riciery Leal/mrmanet. Website indicates 100% free, author confirms it's free for all to use.
|
||||||
|
(https://www.dafont.com/font-comment.php?file=vcr_osd_mono)
|
||||||
@@ -591,6 +591,7 @@
|
|||||||
#include "code\controllers\subsystem\timer.dm"
|
#include "code\controllers\subsystem\timer.dm"
|
||||||
#include "code\controllers\subsystem\title.dm"
|
#include "code\controllers\subsystem\title.dm"
|
||||||
#include "code\controllers\subsystem\traitor.dm"
|
#include "code\controllers\subsystem\traitor.dm"
|
||||||
|
#include "code\controllers\subsystem\tutorials.dm"
|
||||||
#include "code\controllers\subsystem\verb_manager.dm"
|
#include "code\controllers\subsystem\verb_manager.dm"
|
||||||
#include "code\controllers\subsystem\vis_overlays.dm"
|
#include "code\controllers\subsystem\vis_overlays.dm"
|
||||||
#include "code\controllers\subsystem\vote.dm"
|
#include "code\controllers\subsystem\vote.dm"
|
||||||
@@ -4740,6 +4741,10 @@
|
|||||||
#include "code\modules\tgui_panel\telemetry.dm"
|
#include "code\modules\tgui_panel\telemetry.dm"
|
||||||
#include "code\modules\tgui_panel\tgui_panel.dm"
|
#include "code\modules\tgui_panel\tgui_panel.dm"
|
||||||
#include "code\modules\tooltip\tooltip.dm"
|
#include "code\modules\tooltip\tooltip.dm"
|
||||||
|
#include "code\modules\tutorials\_tutorial.dm"
|
||||||
|
#include "code\modules\tutorials\tutorial_instruction.dm"
|
||||||
|
#include "code\modules\tutorials\tutorials\drop.dm"
|
||||||
|
#include "code\modules\tutorials\tutorials\switch_hands.dm"
|
||||||
#include "code\modules\unit_tests\_unit_tests.dm"
|
#include "code\modules\unit_tests\_unit_tests.dm"
|
||||||
#include "code\modules\uplink\uplink_devices.dm"
|
#include "code\modules\uplink\uplink_devices.dm"
|
||||||
#include "code\modules\uplink\uplink_items.dm"
|
#include "code\modules\uplink\uplink_items.dm"
|
||||||
@@ -4970,6 +4975,7 @@
|
|||||||
#include "code\modules\wiremod\shell\shell_items.dm"
|
#include "code\modules\wiremod\shell\shell_items.dm"
|
||||||
#include "code\modules\zombie\items.dm"
|
#include "code\modules\zombie\items.dm"
|
||||||
#include "code\modules\zombie\organs.dm"
|
#include "code\modules\zombie\organs.dm"
|
||||||
|
#include "interface\fonts.dm"
|
||||||
#include "interface\interface.dm"
|
#include "interface\interface.dm"
|
||||||
#include "interface\menu.dm"
|
#include "interface\menu.dm"
|
||||||
#include "interface\stylesheet.dm"
|
#include "interface\stylesheet.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user