More tutorials.

Took 3 hours 23 minutes
This commit is contained in:
Lukas Brübach 2026-01-22 13:52:34 +01:00
parent 80075e4089
commit ca820c52c3
13 changed files with 290 additions and 4 deletions

View File

@ -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;
}
/**

View File

@ -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 <QPushButton>
@ -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);

View File

@ -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)) {

View File

@ -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);

View File

@ -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."});

View File

@ -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()

View File

@ -87,6 +87,8 @@ private:
TutorialOverlay *tutorialOverlay;
QVector<TutorialSequence> sequences;
bool tutorialCompleted = false;
int currentSequence = -1;
int currentStep = -1;

View File

@ -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 <QAction>
@ -129,6 +130,193 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
gameTypes.append(game->getGameMetaInfo()->findRoomGameType(i));
QTimer::singleShot(0, this, &TabGame::loadLayout);
auto mainWindow = QtUtils::findParentOfType<QMainWindow>(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<uint> &));
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)

View File

@ -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<AbstractClient *> &_clients,
const Event_GameJoined &event,
const QMap<int, QString> &_roomGameTypes);
void showEvent(QShowEvent *event);
void finishTutorialInitialization();
void connectToGameState();
void connectToPlayerManager();
void connectToGameEventHandler();

View File

@ -149,6 +149,10 @@ public:
{
return userListManager;
}
[[nodiscard]] TabHome *getTabHome() const
{
return tabHome;
}
[[nodiscard]] const QMap<int, TabRoom *> &getRoomTabs() const
{
return roomTabs;

View File

@ -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)

View File

@ -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) {

View File

@ -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;