diff --git a/cockatrice/src/game/deckview/deck_view_container.cpp b/cockatrice/src/game/deckview/deck_view_container.cpp index 44b2be6d1..422122e01 100644 --- a/cockatrice/src/game/deckview/deck_view_container.cpp +++ b/cockatrice/src/game/deckview/deck_view_container.cpp @@ -99,6 +99,21 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) &DeckViewContainer::setVisualDeckStorageExists); switchToDeckSelectView(); + generateTutorialSequence(); +} + +TutorialSequence DeckViewContainer::generateTutorialSequence() +{ + TutorialSequence deckViewContainerSequence; + deckViewContainerSequence.name = tr("Loading and selecting decks"); + + deckViewContainerSequence.addStep( + {this, tr("There are multiple ways to select a deck:\n\n- From a local file" + "\n- From the contents of your clipboard\nFrom an external online service")}); + + deckViewContainerSequence = visualDeckStorageWidget->generateTutorialSequence(deckViewContainerSequence); + + return deckViewContainerSequence; } /** diff --git a/cockatrice/src/game/deckview/deck_view_container.h b/cockatrice/src/game/deckview/deck_view_container.h index 6d685cd79..d675037b8 100644 --- a/cockatrice/src/game/deckview/deck_view_container.h +++ b/cockatrice/src/game/deckview/deck_view_container.h @@ -8,6 +8,7 @@ #define DECK_VIEW_CONTAINER_H #include "../../interface/deck_loader/deck_loader.h" +#include "../../interface/widgets/general/tutorial/tutorial_controller.h" #include @@ -82,6 +83,7 @@ signals: public: DeckViewContainer(int _playerId, TabGame *parent); void retranslateUi(); + TutorialSequence generateTutorialSequence(); void setReadyStart(bool ready); void readyAndUpdate(); void setSideboardLocked(bool locked); diff --git a/cockatrice/src/game/deckview/tabbed_deck_view_container.cpp b/cockatrice/src/game/deckview/tabbed_deck_view_container.cpp index 2ffb214f7..7ab89fe5e 100644 --- a/cockatrice/src/game/deckview/tabbed_deck_view_container.cpp +++ b/cockatrice/src/game/deckview/tabbed_deck_view_container.cpp @@ -15,6 +15,11 @@ TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent) updateTabBarVisibility(); } +TutorialSequence TabbedDeckViewContainer::generateTutorialSequence() +{ + return playerDeckView->generateTutorialSequence(); +} + void TabbedDeckViewContainer::addOpponentDeckView(const DeckList &opponentDeck, int opponentId, QString opponentName) { if (opponentDeckViews.contains(opponentId)) { diff --git a/cockatrice/src/game/deckview/tabbed_deck_view_container.h b/cockatrice/src/game/deckview/tabbed_deck_view_container.h index c34eef1ef..c320e4b02 100644 --- a/cockatrice/src/game/deckview/tabbed_deck_view_container.h +++ b/cockatrice/src/game/deckview/tabbed_deck_view_container.h @@ -16,6 +16,7 @@ class TabbedDeckViewContainer : public QTabWidget public: explicit TabbedDeckViewContainer(int _playerId, TabGame *parent); + TutorialSequence generateTutorialSequence(); void closeTab(int index); void updateTabBarVisibility(); void addOpponentDeckView(const DeckList &opponentDeck, int opponentId, QString opponentName); diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index d202a4e52..afb1831af 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -60,6 +60,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) vdeStep.requiresInteraction = true; vdeStep.allowClickThrough = true; vdeStep.validationHint = "Open the deck editor to try it out!"; + vdeStep.validationTiming = ValidationTiming::OnSignal; + vdeStep.autoAdvanceOnValid = true; + vdeStep.validator = []() { return true; }; + vdeStep.signalSource = visualDeckEditorButton; + vdeStep.signalName = SIGNAL(clicked()); + sequence.addStep(vdeStep); sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."}); sequence.addStep({visualDatabaseDisplayButton, "View the card database here."}); diff --git a/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.cpp b/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.cpp index c9767b462..8839f7809 100644 --- a/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.cpp +++ b/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.cpp @@ -31,7 +31,7 @@ void TutorialController::addSequence(const TutorialSequence &seq) void TutorialController::start() { - if (sequences.isEmpty()) { + if (sequences.isEmpty() || tutorialCompleted) { return; } @@ -110,17 +110,21 @@ bool TutorialController::validateCurrentStep() void TutorialController::nextStep() { - currentStep++; - if (currentSequence < 0) { return; } - if (currentStep >= sequences[currentSequence].steps.size()) { + if (currentStep >= sequences[currentSequence].steps.size() - 1) { + // We're on the last step of this sequence, run its onExit before advancing + const auto &lastStep = sequences[currentSequence].steps[currentStep]; + if (lastStep.onExit) { + lastStep.onExit(); + } nextSequence(); return; } + currentStep++; showStep(); } @@ -183,6 +187,7 @@ void TutorialController::exitTutorial() tutorialOverlay->hide(); currentSequence = -1; currentStep = -1; + tutorialCompleted = true; } void TutorialController::updateProgress() diff --git a/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h b/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h index 838756e3a..1bb2ecfb5 100644 --- a/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h +++ b/cockatrice/src/interface/widgets/general/tutorial/tutorial_controller.h @@ -87,6 +87,8 @@ private: TutorialOverlay *tutorialOverlay; QVector sequences; + bool tutorialCompleted = false; + int currentSequence = -1; int currentStep = -1; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 2df6e5ac5..7f66d0f44 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -21,6 +21,7 @@ #include "../interface/window_main.h" #include "../main.h" #include "../utility/visibility_change_listener.h" +#include "libcockatrice/utility/qt_utils.h" #include "tab_supervisor.h" #include @@ -129,6 +130,193 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, gameTypes.append(game->getGameMetaInfo()->findRoomGameType(i)); QTimer::singleShot(0, this, &TabGame::loadLayout); + + auto mainWindow = QtUtils::findParentOfType(this); + + if (mainWindow) { + tutorialController = new TutorialController(mainWindow); + } else { + tutorialController = new TutorialController(this); + } + + TutorialSequence lobbySequence; + + TutorialStep introStep(deckViewContainerWidget, tr("Let's try this out.")); + lobbySequence.addStep(introStep); + + tutorialController->addSequence(lobbySequence); +} + +void TabGame::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + if (!tutorialStarted) { + tutorialStarted = true; + // Start on next event loop iteration so everything is fully painted + QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); }); + } +} + +void TabGame::finishTutorialInitialization() +{ + if (tutorialInitialized) { + return; + } else { + tutorialInitialized = true; + } + + auto deckViewSequence = deckViewContainers.first()->generateTutorialSequence(); + + tutorialController->addSequence(deckViewSequence); + + TutorialSequence deckSelectSequence; + deckSelectSequence.name = tr("Deck selection and readying up"); + + TutorialStep loadDeckStep; + loadDeckStep.targetWidget = deckViewContainers.first(); + loadDeckStep.text = tr("Let's load a deck now."); + loadDeckStep.allowClickThrough = true; + loadDeckStep.requiresInteraction = true; + loadDeckStep.autoAdvanceOnValid = true; + loadDeckStep.validationTiming = ValidationTiming::OnSignal; + loadDeckStep.signalSource = game->getGameEventHandler(); + loadDeckStep.signalName = SIGNAL(logDeckSelect(Player *, QString, int)); + loadDeckStep.validator = [] { return true; }; + + deckSelectSequence.addStep(loadDeckStep); + + TutorialStep readyUpStep; + readyUpStep.targetWidget = deckViewContainers.first(); + readyUpStep.text = tr("Let's ready up now."); + readyUpStep.allowClickThrough = true; + readyUpStep.requiresInteraction = true; + readyUpStep.autoAdvanceOnValid = true; + readyUpStep.validationTiming = ValidationTiming::OnSignal; + readyUpStep.signalSource = this; + readyUpStep.signalName = SIGNAL(localPlayerReadyStateChanged(bool)); + readyUpStep.validator = [] { return true; }; + + deckSelectSequence.addStep(readyUpStep); + + tutorialController->addSequence(deckSelectSequence); + + TutorialSequence gamePlaySequence; + gamePlaySequence.name = tr("Gameplay"); + + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("Welcome to your first game! It's just a singleplayer game for now to teach you the controls.")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("Welcome to your first game! It's just a singleplayer game for now to teach you the controls.")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("Unfortunately, due to the way the game tab works, we can't highlight any specific gameplay elements but " + "we're confident you'll be able to spot all the relevant elements on-screen.")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("Let's go over them quickly, left-to-right.\n\nThe phase toolbar\nThe player area\nThe battlefield")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("First up, is the phase toolbar. This toolbar shows the current phase of the turn. You " + "can advance it by pressing\n\n- Tab (simply advances the phase)\n- Ctrl+Space (advances " + "the phase and performes any associated actions)\n- Clicking directly on the phase you " + "want to change to.\n\nYou can also pass the turn here, although, you should note that " + "most players prefer you simply leave your turn on the end step and allow them to " + "'take' the turn from you by pressing 'Next turn' themselves.")}); + gamePlaySequence.addStep({gamePlayAreaWidget, tr("Next up, is your player area.\n\nHere you can find:\n\n- Your " + "avatar\n- Your life-counter\n- Various counters you can use to " + "track temporary resources (i.e. mana)\n- Your " + "library,\n- Your hand\n- Your graveyard\n- Your exile")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("To the right of your player area, and taking up most of the screen, is your battlefield.\nThe relevant " + "zones here are, left-to-right, top-to-bottom:\n- The Stack\n- The Battlefield (Currently highlighted " + "because it is your turn)\n- Your Hand")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, + tr("Before we dive any deeper into the actual controls, remember this:\n\nYou can perform almost " + "EVERY action by right-clicking the relevant object or zone!")}); + gamePlaySequence.addStep( + {gamePlayAreaWidget, tr("However, there are shortcuts and conveniences to speed up your games and make your " + "life easier.\n\nLet's run through a typical game start now to get you up to speed.")}); + + TutorialStep lifeCounterStep; + lifeCounterStep.targetWidget = gamePlayAreaWidget; + lifeCounterStep.text = + tr("To control your life total, you can:\n\nSet it directly using Ctrl+L\nLeft-click the " + "number on your avatar to increment it.\nRight-click the number on your avatar to decrement " + "it.\nMiddle-click the number on your avatar to open up an interval menu up to +-10."); + lifeCounterStep.requiresInteraction = true; + lifeCounterStep.allowClickThrough = true; + lifeCounterStep.autoAdvanceOnValid = true; + lifeCounterStep.validationTiming = ValidationTiming::OnSignal; + lifeCounterStep.signalSource = game->getPlayerManager() + ->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId()) + ->getPlayerEventHandler(); + lifeCounterStep.signalName = SIGNAL(logSetCounter(Player *, QString, int, int)); + lifeCounterStep.validator = [this] { + auto counters = + game->getPlayerManager()->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())->getCounters(); + for (auto counter : counters) { + if (counter->getName() == "life") { + return counter->getValue() == 10; + } + } + return false; + }; + lifeCounterStep.validationHint = tr("Set your life total to 10 using any of these methods."); + + gamePlaySequence.addStep(lifeCounterStep); + + TutorialStep diceRollStep; + diceRollStep.targetWidget = gamePlayAreaWidget; + diceRollStep.text = tr("Fantastic! Let's roll a dice now. Many players use this to determine the initial turn " + "order.\nYou can right-click the battlefield and choose the menu " + "option or use the shortcut (Default Ctrl+I)."); + diceRollStep.requiresInteraction = true; + diceRollStep.allowClickThrough = true; + diceRollStep.autoAdvanceOnValid = true; + diceRollStep.validationTiming = ValidationTiming::OnSignal; + diceRollStep.signalSource = game->getPlayerManager() + ->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId()) + ->getPlayerEventHandler(); + diceRollStep.signalName = SIGNAL(logRollDie(Player *, int, const QList &)); + diceRollStep.validator = [this] { return true; }; + diceRollStep.validationHint = tr("Roll a dice using any of these methods."); + + gamePlaySequence.addStep(diceRollStep); + + TutorialStep mulliganStep; + mulliganStep.targetWidget = gamePlayAreaWidget; + mulliganStep.text = + tr("Alright, with that out of the way, we can get down to business:\n\nDrawing cards!\n\nTo draw your initial " + "hand:\n\n- Right-click your hand in the player area and select 'Take mulligan'\n-n Right-click your hand " + "zone on the battlefield and select 'Take mulligan'\n- Use the default shortcut (Ctrl+M)"); + mulliganStep.requiresInteraction = true; + mulliganStep.allowClickThrough = true; + mulliganStep.autoAdvanceOnValid = true; + mulliganStep.validationTiming = ValidationTiming::OnSignal; + mulliganStep.signalSource = game->getPlayerManager() + ->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId()) + ->getPlayerEventHandler(); + mulliganStep.signalName = SIGNAL(logDrawCards(Player *, int, bool)); + mulliganStep.validator = [this] { + return game->getPlayerManager() + ->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId()) + ->getHandZone() + ->getCards() + .size() == 7; + }; + mulliganStep.validationHint = tr("Mulligan to 7 cards using any of these methods."); + + gamePlaySequence.addStep(mulliganStep); + + gamePlaySequence.addStep({gamePlayAreaWidget, tr("")}); + gamePlaySequence.addStep({gamePlayAreaWidget, tr("")}); + + gamePlaySequence.addStep({gamePlayAreaWidget, tr("")}); + tutorialController->addSequence(gamePlaySequence); } void TabGame::connectToGameState() @@ -687,6 +875,8 @@ void TabGame::addLocalPlayer(Player *newPlayer, int playerId) deckView->playerDeckView->readyAndUpdate(); }); } + + finishTutorialInitialization(); } void TabGame::processPlayerLeave(Player *leavingPlayer) @@ -750,6 +940,8 @@ void TabGame::loadDeckForLocalPlayer(Player *localPlayer, int playerId, ServerIn CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(deckList.getCardRefList())); deckViewContainer->playerDeckView->setDeck(deckList); localPlayer->setDeck(deckList); + + emit localPlayerDeckSelected(); } } @@ -767,6 +959,7 @@ void TabGame::processLocalPlayerSideboardLocked(int playerId, bool sideboardLock void TabGame::processLocalPlayerReadyStateChanged(int playerId, bool ready) { deckViewContainers.value(playerId)->playerDeckView->setReadyStart(ready); + emit localPlayerReadyStateChanged(ready); } void TabGame::createZoneForPlayer(Player *newPlayer, int playerId) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 4f944bf87..bce2ec4e4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -57,6 +57,9 @@ class TabGame : public Tab Q_OBJECT private: AbstractGame *game; + TutorialController *tutorialController; + bool tutorialStarted = false; + bool tutorialInitialized = false; const UserListProxy *userListProxy; ReplayManager *replayManager = nullptr; QStringList gameTypes; @@ -125,6 +128,8 @@ private: void createDeckViewContainerWidget(bool bReplay = false); void createReplayDock(GameReplay *replay); signals: + void localPlayerDeckSelected(); + void localPlayerReadyStateChanged(bool ready); void gameClosing(TabGame *tab); void containerProcessingStarted(const GameEventContext &context); void containerProcessingDone(); @@ -176,6 +181,8 @@ public: QList &_clients, const Event_GameJoined &event, const QMap &_roomGameTypes); + void showEvent(QShowEvent *event); + void finishTutorialInitialization(); void connectToGameState(); void connectToPlayerManager(); void connectToGameEventHandler(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 0c4428f83..a905a9f01 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -149,6 +149,10 @@ public: { return userListManager; } + [[nodiscard]] TabHome *getTabHome() const + { + return tabHome; + } [[nodiscard]] const QMap &getRoomTabs() const { return roomTabs; diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 53dc24b04..1ef0039ca 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -9,6 +9,7 @@ #include "../../interface/widgets/general/tutorial/tutorial_controller.h" #include "../../interface/widgets/visual_deck_editor/visual_deck_editor_widget.h" #include "../tab_deck_editor.h" +#include "../tab_home.h" #include "../tab_supervisor.h" #include "tab_deck_editor_visual_tab_widget.h" @@ -101,6 +102,24 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra auto sampleHandSequence = tabContainer->sampleHandWidget->generateTutorialSequence(); tutorialController->addSequence(sampleHandSequence); + + TutorialSequence endSequence; + endSequence.name = tr("Visual Deck Editor Conclusion"); + + TutorialStep introStep; + introStep.targetWidget = this; + introStep.text = tr("This concludes the Visual Deck Editor tutorial."); + introStep.onEnter = [this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); }; + endSequence.addStep(introStep); + + TutorialStep conclusionStep; + conclusionStep.targetWidget = tabSupervisor->tabBar(); + conclusionStep.text = + tr("Let's go back to the Home Tab now to explore where you can manage your newly created deck."); + conclusionStep.onExit = [this]() { tabSupervisor->setCurrentWidget(tabSupervisor->getTabHome()); }; + endSequence.addStep(conclusionStep); + + tutorialController->addSequence(endSequence); } void TabDeckEditorVisual::showEvent(QShowEvent *ev) diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index bfc425993..bb2b3aa3e 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -1,6 +1,7 @@ #include "visual_deck_storage_widget.h" #include "../../../client/settings/cache_settings.h" +#include "../general/tutorial/tutorial_controller.h" #include "../quick_settings/settings_button_widget.h" #include "deck_preview/deck_preview_widget.h" #include "visual_deck_storage_folder_display_widget.h" @@ -88,6 +89,30 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare } } +TutorialSequence VisualDeckStorageWidget::generateTutorialSequence(TutorialSequence vdsSequence) +{ + vdsSequence.addStep( + {this, tr("This is the visual deck storage. It displays all the files and folders located in " + "your default deck storage location. You can adjust this location in the settings.")}); + vdsSequence.addStep({searchAndSortContainer, + tr("You can filter the decks in your collection using these widgets. Check the (i) symbol in " + "the search bar for more information on the syntax used to filter decks.")}); + vdsSequence.addStep( + {tagFilterWidget, + tr("Additionally, the VDS allows you to assign and filter by tags. This is very helpful for large deck " + "collections, as it allows you to group similar decks (i.e. by power level or theme) and then 'drill down' " + "to exactly the combination of tags that interests you (i.e. 'I want to play a mid-power deck focused on " + "this type of card that wins with this strategy.')\n\nYou can left-click a tag to add it to the " + "filter.\nThe widget will then automatically filter the list to only include tags from decks which also " + "contain your original tag.\n\nYou can exclude a tag by right-clicking it.\n\nYou can clear a tags filter " + "status with the middle mouse button.")}); + vdsSequence.addStep({scrollArea, tr("This is where all your local decks will be displayed. You can customize their " + "display status using the cogwheel in the top right.\nYou can select a deck by " + "double-clicking it.\n\nRight-click a deck for more options.")}); + + return vdsSequence; +} + void VisualDeckStorageWidget::refreshIfPossible() { if (scrollArea->widget() != databaseLoadIndicator) { diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h index b13c51700..a2ac1a18f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -9,6 +9,7 @@ #include "../../deck_loader/deck_loader.h" #include "../cards/card_size_widget.h" +#include "../general/tutorial/tutorial_controller.h" #include "../quick_settings/settings_button_widget.h" #include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "visual_deck_storage_folder_display_widget.h" @@ -32,6 +33,7 @@ public: explicit VisualDeckStorageWidget(QWidget *parent); void refreshIfPossible(); void retranslateUi(); + TutorialSequence generateTutorialSequence(TutorialSequence vdsSequence); VisualDeckStorageTagFilterWidget *tagFilterWidget; bool deckPreviewSelectionAnimationEnabled;