feat: implement Deck Builder and Collection tutorial phases

- Added DECK_BUILDER_INTRO (7) and COLLECTION_INTRO (8) stages to TutorialManager.STAGES
- Implemented 3-phase Deck Builder tutorial in DeckBuilderScene with dimming overlays, pulsing arrows, and step-by-step guidance for deck management
- Added full-screen tutorial flow in MainMenuScene for both Deck Builder intro (with interactive button override) and Collection intro (highlighting save/restore buttons)
- Integrated tutorial progression logic to advance users through remaining stages after Fusion Intro
This commit is contained in:
Brian Fertig 2026-03-31 23:24:13 -06:00
parent 0ea053dc44
commit 307704b90d
3 changed files with 458 additions and 1 deletions

View File

@ -1,7 +1,7 @@
import { SaveManager } from './SaveManager.js';
export class TutorialManager {
static STAGES = { CAMPAIGN_INTRO: 1, CAMPAIGN_SELECT: 2, BATTLE_INTRO: 3, BATTLEFIELD_GUIDE: 4, STORE_INTRO: 5, FUSION_INTRO: 6 };
static STAGES = { CAMPAIGN_INTRO: 1, CAMPAIGN_SELECT: 2, BATTLE_INTRO: 3, BATTLEFIELD_GUIDE: 4, STORE_INTRO: 5, FUSION_INTRO: 6, DECK_BUILDER_INTRO: 7, COLLECTION_INTRO: 8 };
static isStageComplete(save, stageNum) {
return save.tutorialProgress?.completedStages?.includes(stageNum) ?? false;

View File

@ -1,6 +1,7 @@
import { DeckManager } from '../managers/DeckManager.js';
import { SaveManager } from '../managers/SaveManager.js';
import { CardObject } from '../objects/CardObject.js';
import { TutorialManager } from '../managers/TutorialManager.js';
export class DeckBuilderScene extends Phaser.Scene {
constructor() { super('DeckBuilderScene'); }
@ -77,6 +78,9 @@ export class DeckBuilderScene extends Phaser.Scene {
this._renderDeckPanel();
this._makeButtons();
this._setupScrolling();
this._tutorialElements = [];
this._showTutorial();
}
_rebuildUsedCounts() {
@ -627,4 +631,222 @@ export class DeckBuilderScene extends Phaser.Scene {
}).setOrigin(0.5);
this.time.delayedCall(2000, () => { if (t.scene) t.destroy(); });
}
_showTutorial() {
const save = this.registry.get('save');
if (
TutorialManager.isStageComplete(save, TutorialManager.STAGES.FUSION_INTRO) &&
!TutorialManager.isStageComplete(save, TutorialManager.STAGES.DECK_BUILDER_INTRO)
) {
this._showDeckBuilderPhase1();
}
}
_clearTutorialElements() {
for (const el of this._tutorialElements) {
if (el && el.scene) el.destroy();
}
this._tutorialElements = [];
}
_showDeckBuilderPhase1() {
this._clearTutorialElements();
const { width, height } = this.scale;
const DEPTH = 500;
const PANEL_X = 1340;
const panelCX = PANEL_X + 540 / 2; // 1610
const gridCX = PANEL_X / 2; // ~670 — center of the dimmed left area
// Dim the left card grid area
const dimLeft = this.add.rectangle(gridCX, height / 2, PANEL_X, height, 0x000000, 0.78)
.setDepth(DEPTH);
// Instructional text overlaid on the dimmed left area
const text = this.add.text(gridCX, 490, [
'This is your Current Deck panel.',
'Here you can see your commander and',
'all assault cards in your deck.',
'Use the ( \u2212 ) buttons to remove cards.'
].join('\n'), {
fontSize: '22px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 8,
wordWrap: { width: 600 }
}).setOrigin(0.5).setDepth(DEPTH + 1);
// Continue button
const contBg = this.add.rectangle(gridCX, 640, 200, 46, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff)
.setDepth(DEPTH + 1);
const contTxt = this.add.text(gridCX, 640, 'Continue', {
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: contBg,
alpha: { from: 0.7, to: 1 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Pulsing arrow just left of the Current Deck panel, pointing right
const arrow = this.add.text(PANEL_X - 28, height / 2, '\u25BA', {
fontSize: '42px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: arrow,
x: PANEL_X - 14,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
contBg.on('pointerdown', () => this._showDeckBuilderPhase2());
this._tutorialElements = [dimLeft, text, contBg, contTxt, arrow];
}
_showDeckBuilderPhase2() {
this._clearTutorialElements();
const { width, height } = this.scale;
const DEPTH = 500;
const PANEL_X = 1340;
const panelCX = PANEL_X + 540 / 2; // 1610 — center of the dimmed right area
const gridCX = PANEL_X / 2; // ~670
// Dim the right deck panel area
const rightW = width - PANEL_X;
const dimRight = this.add.rectangle(PANEL_X + rightW / 2, height / 2, rightW, height, 0x000000, 0.78)
.setDepth(DEPTH);
// Dim the top header
const dimTop = this.add.rectangle(gridCX, this.SCROLL_TOP / 2, PANEL_X, this.SCROLL_TOP, 0x000000, 0.78)
.setDepth(DEPTH);
// Instructional text overlaid on the dimmed right area
const text = this.add.text(panelCX, 490, [
'This is your card collection.',
'Cards you don\'t own are grayed out.',
'Use the ( + ADD ) button to add',
'a card to your deck.'
].join('\n'), {
fontSize: '22px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 8,
wordWrap: { width: 500 }
}).setOrigin(0.5).setDepth(DEPTH + 1);
// Continue button
const contBg = this.add.rectangle(panelCX, 640, 200, 46, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff)
.setDepth(DEPTH + 1);
const contTxt = this.add.text(panelCX, 640, 'Continue', {
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: contBg,
alpha: { from: 0.7, to: 1 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Pulsing arrow just right of the card collection boundary, pointing left
const arrow = this.add.text(PANEL_X + 28, height / 2, '\u25C4', {
fontSize: '42px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: arrow,
x: PANEL_X + 14,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
contBg.on('pointerdown', () => this._showDeckBuilderPhase3());
this._tutorialElements = [dimRight, dimTop, text, contBg, contTxt, arrow];
}
_showDeckBuilderPhase3() {
this._clearTutorialElements();
const { width, height } = this.scale;
const DEPTH = 500;
const PANEL_X = 1340;
const PANEL_W = 540;
const panelCX = PANEL_X + PANEL_W / 2; // 1610
const saveBtnY = height - 60; // 1020
// Button highlight area: Save (cx=1500, w=190) to Clear (cx=1720, w=190)
const btnAreaLeft = 1500 - 95 - 10; // 1395
const btnAreaRight = 1720 + 95 + 10; // 1825
const btnAreaTop = saveBtnY - 28; // 992
const btnAreaBottom = saveBtnY + 28; // 1048
// Targeted dimming — four rects around the highlight area
const dimTop = this.add.rectangle(width / 2, btnAreaTop / 2, width, btnAreaTop, 0x000000, 0.78)
.setDepth(DEPTH);
const dimBottom = this.add.rectangle(width / 2, (btnAreaBottom + height) / 2, width, height - btnAreaBottom, 0x000000, 0.78)
.setDepth(DEPTH);
const dimLeft = this.add.rectangle(btnAreaLeft / 2, saveBtnY, btnAreaLeft, btnAreaBottom - btnAreaTop, 0x000000, 0.78)
.setDepth(DEPTH);
const rightW = width - btnAreaRight;
const dimRight = this.add.rectangle(btnAreaRight + rightW / 2, saveBtnY, rightW, btnAreaBottom - btnAreaTop, 0x000000, 0.78)
.setDepth(DEPTH);
// Instructional text above the button area (rendered above the dim due to higher depth)
const text = this.add.text(panelCX, 860, [
'Use Save Deck to keep your changes,',
'and Clear Deck to start over.',
'Make sure to save before leaving!'
].join('\n'), {
fontSize: '22px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 8,
wordWrap: { width: 520 }
}).setOrigin(0.5).setDepth(DEPTH + 1);
// Continue button — completes the tutorial
const contBg = this.add.rectangle(panelCX, 960, 200, 46, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff)
.setDepth(DEPTH + 1);
const contTxt = this.add.text(panelCX, 960, 'Continue', {
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: contBg,
alpha: { from: 0.7, to: 1 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
contBg.on('pointerdown', () => {
const save = this.registry.get('save');
TutorialManager.completeStage(save, TutorialManager.STAGES.DECK_BUILDER_INTRO);
this.registry.set('save', save);
this._clearTutorialElements();
});
this._tutorialElements = [dimTop, dimBottom, dimLeft, dimRight, text, contBg, contTxt];
}
}

View File

@ -154,6 +154,16 @@ export class MainMenuScene extends Phaser.Scene {
TutorialManager.hasFusibleCards(save)
) {
this._showFusionIntroTutorial(save);
} else if (
TutorialManager.isStageComplete(save, TutorialManager.STAGES.FUSION_INTRO) &&
!TutorialManager.isStageComplete(save, TutorialManager.STAGES.DECK_BUILDER_INTRO)
) {
this._showDeckBuilderIntroTutorial(save);
} else if (
TutorialManager.isStageComplete(save, TutorialManager.STAGES.DECK_BUILDER_INTRO) &&
!TutorialManager.isStageComplete(save, TutorialManager.STAGES.COLLECTION_INTRO)
) {
this._showCollectionIntroTutorial(save);
}
}
@ -470,6 +480,231 @@ export class MainMenuScene extends Phaser.Scene {
});
}
_showCollectionIntroTutorial(save) {
this._showCollectionPhase1();
}
_showCollectionPhase1() {
const { width, height } = this.scale;
const DEPTH = 1000;
// Collection is index 3 (startY=310, spacing=90 → y=580)
const btnX = width / 2;
const btnY = 310 + 3 * 90; // 580
const btnHalfW = 200;
// Full-screen overlay — blocks all clicks including the Collection button
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78)
.setDepth(DEPTH).setInteractive();
// Duplicate Collection button — visual only, NOT interactive
const btnBg = this.add.rectangle(btnX, btnY, btnHalfW * 2, 65, 0x1a3a5c)
.setDepth(DEPTH + 1).setStrokeStyle(2, 0x4488ff);
const btnText = this.add.text(btnX, btnY, 'Collection', {
fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
// Arrow to the right of the button, pointing left at it (◄)
const arrowX = btnX + btnHalfW + 70; // ~1230
const arrow = this.add.text(arrowX, btnY, '\u25C4', {
fontSize: '42px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: arrow,
x: arrowX - 14,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Descriptive text to the right of the arrow
const textX = arrowX + 180; // ~1410
const text = this.add.text(textX, btnY - 70, [
'The Collection shows every card',
'you have ever acquired.',
'Browse by faction, rarity, or type',
'to review what you\'ve unlocked.'
].join('\n'), {
fontSize: '22px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 8,
wordWrap: { width: 460 }
}).setOrigin(0.5, 0).setDepth(DEPTH + 1);
// Continue button below text
const contBg = this.add.rectangle(textX, btnY + 130, 200, 46, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff)
.setDepth(DEPTH + 1);
const contTxt = this.add.text(textX, btnY + 130, 'Continue', {
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: contBg,
alpha: { from: 0.7, to: 1 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
const els = [overlay, btnBg, btnText, arrow, text, contBg, contTxt];
contBg.on('pointerdown', () => {
this.sound.play('sfx_menu_select', { volume: 0.7 });
els.forEach(el => { if (el.scene) el.destroy(); });
this._showCollectionPhase2();
});
}
_showCollectionPhase2() {
const { width, height } = this.scale;
const DEPTH = 1000;
// Save/Restore buttons: BW=250, x=20+125=145, y=25 and y=65
// Area bounds: left=20, right=270, top=8, bottom=82
const areaLeft = 20;
const areaRight = 270;
const areaBottom = 90; // slightly generous
const areaCX = (areaLeft + areaRight) / 2; // 145
// Targeted dimming — leave top-left corner visible
const dimTopRight = this.add.rectangle(
(areaRight + width) / 2, areaBottom / 2,
width - areaRight, areaBottom,
0x000000, 0.78
).setDepth(DEPTH);
const dimBelow = this.add.rectangle(
width / 2, (areaBottom + height) / 2,
width, height - areaBottom,
0x000000, 0.78
).setDepth(DEPTH);
// Arrow just below the save/restore area, pointing up
const arrowY = areaBottom + 30; // 120
const arrow = this.add.text(areaCX, arrowY, '\u25B2', {
fontSize: '36px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: arrow,
y: arrowY - 12,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Explanatory text below arrow
const text = this.add.text(areaCX, arrowY + 50, [
'Your game saves automatically in',
'your browser\'s local storage.',
'Use these buttons to save a backup',
'to your hard drive, or restore',
'a previously saved file.'
].join('\n'), {
fontSize: '18px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 6,
wordWrap: { width: 300 }
}).setOrigin(0.5, 0).setDepth(DEPTH + 1);
// Continue button — completes Stage 8
const contY = arrowY + 260;
const contBg = this.add.rectangle(areaCX, contY, 200, 46, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff)
.setDepth(DEPTH + 1);
const contTxt = this.add.text(areaCX, contY, 'Continue', {
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: contBg,
alpha: { from: 0.7, to: 1 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
const els = [dimTopRight, dimBelow, arrow, text, contBg, contTxt];
contBg.on('pointerdown', () => {
this.sound.play('sfx_menu_select', { volume: 0.7 });
const save = this.registry.get('save');
TutorialManager.completeStage(save, TutorialManager.STAGES.COLLECTION_INTRO);
this.registry.set('save', save);
els.forEach(el => { if (el.scene) el.destroy(); });
});
}
_showDeckBuilderIntroTutorial(save) {
const { width, height } = this.scale;
const DEPTH = 1000;
// Deck Builder is index 2 in the button list (startY=310, spacing=90)
const btnX = width / 2;
const btnY = 310 + 2 * 90; // 490
// Dark overlay — blocks all clicks beneath
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78)
.setDepth(DEPTH)
.setInteractive();
// Duplicate Deck Builder button on top of overlay
const btnBg = this.add.rectangle(btnX, btnY, 400, 65, 0x1a3a5c)
.setDepth(DEPTH + 1)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff);
const btnText = this.add.text(btnX, btnY, 'Deck Builder', {
fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 1);
btnBg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); btnBg.setFillStyle(0x2a5a8c); });
btnBg.on('pointerout', () => btnBg.setFillStyle(0x1a3a5c));
// Pulsing arrow above button
const arrow = this.add.text(btnX, btnY - 60, '\u25BC', {
fontSize: '48px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this.tweens.add({
targets: arrow,
y: btnY - 45,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Instructional text
const infoText = this.add.text(btnX, btnY + 55, [
'The Deck Builder lets you customize your deck.',
'Add or remove cards from your collection,',
'choose your commander, and save your changes.',
'Click Deck Builder to explore it!'
].join('\n'), {
fontSize: '22px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 8
}).setOrigin(0.5, 0).setDepth(DEPTH + 1);
const tutorialElements = [overlay, btnBg, btnText, arrow, infoText];
btnBg.on('pointerdown', () => {
this.sound.play('sfx_menu_select', { volume: 0.7 });
tutorialElements.forEach(el => el.destroy());
this.scene.start('DeckBuilderScene');
});
}
_makeFullscreenButton(x, y) {
const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen';