feat: add tutorial stages for Store intro (5) and Fusion intro (6)

- Extended TutorialManager to support new stages 5–6 and added hasFusibleCards() helper
- Integrated Store tutorial in MainMenuScene (triggered when gold > 400, redirects to Store)
- Added full interactive tutorial overlay in StoreScene highlighting imperial pack purchase flow
- Implemented Fusion Scene’s two-phase tutorial: phase 1 highlights fusible stacks with pulsing borders/arrows; phase 2 exposes forge video, preview, and Fuse button with instructions
- Updated BattleResultScene to redirect to Main Menu if gold > 400 and store tutorial incomplete, allowing Store scene to show its own tutorial next
- Completed stage 6 when player clicks Fuse button in FusionScene
This commit is contained in:
Brian Fertig 2026-03-31 23:01:32 -06:00
parent 5b206fda33
commit 0ea053dc44
5 changed files with 516 additions and 2 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 };
static STAGES = { CAMPAIGN_INTRO: 1, CAMPAIGN_SELECT: 2, BATTLE_INTRO: 3, BATTLEFIELD_GUIDE: 4, STORE_INTRO: 5, FUSION_INTRO: 6 };
static isStageComplete(save, stageNum) {
return save.tutorialProgress?.completedStages?.includes(stageNum) ?? false;
@ -14,4 +14,21 @@ export class TutorialManager {
}
SaveManager.save(save);
}
// Returns true if the player has ≥3 copies of any single non-commander card
// that are not locked in a deck — i.e., eligible for fusion.
static hasFusibleCards(save) {
const used = {};
for (const deck of (save.decks || [])) {
if (deck.commander) used[deck.commander] = (used[deck.commander] || 0) + 1;
for (const cid of (deck.cards || [])) {
used[cid] = (used[cid] || 0) + 1;
}
}
for (const [cardId, owned] of Object.entries(save.collection || {})) {
const available = owned - (used[cardId] || 0);
if (available >= 3) return true;
}
return false;
}
}

View File

@ -1,4 +1,5 @@
import { CardObject } from '../objects/CardObject.js';
import { TutorialManager } from '../managers/TutorialManager.js';
// ── Amber CRT Palette ───────────────────────────────────────────────────────────
const AMBER = '#ffaa33';
@ -974,6 +975,15 @@ export class BattleResultScene extends Phaser.Scene {
if (e.stop) e.stop();
}
// Stage 5 tutorial redirect: if player has enough gold and hasn't seen store tutorial yet
const save = this.registry.get('save');
const needsStoreTutorial = save.gold > 400 &&
!TutorialManager.isStageComplete(save, TutorialManager.STAGES.STORE_INTRO);
if (needsStoreTutorial) {
this.scene.start('MainMenuScene');
return;
}
if (!this.missionData) {
// Skirmish → Main Menu
this.scene.start('MainMenuScene');

View File

@ -1,5 +1,6 @@
import { SaveManager } from '../managers/SaveManager.js';
import { CardObject } from '../objects/CardObject.js';
import { TutorialManager } from '../managers/TutorialManager.js';
const FUSION_RECIPES = {
common: { count: 3, result_rarity: 'rare' },
@ -99,6 +100,11 @@ export class FusionScene extends Phaser.Scene {
this._setupScrolling();
this._generateParticleTexture();
this._renderStacks();
// Stage 6 tutorial — show after stacks are rendered so positions are known
this._tutorialOverlayElements = [];
this._tutorialPhase = null;
this.time.delayedCall(200, () => this._showTutorialPhase1());
}
// ── Header ─────────────────────────────────────────────────────────────────
@ -543,6 +549,11 @@ export class FusionScene extends Phaser.Scene {
this._fuseInfoText.setVisible(true);
this._fuseBtnBg.setVisible(true);
this._fuseBtnText.setVisible(true);
// Advance stage 6 tutorial from phase 1 → phase 2
if (this._tutorialPhase === 1) {
this.time.delayedCall(100, () => this._showTutorialPhase2());
}
}
_deselectStack() {
@ -662,6 +673,9 @@ export class FusionScene extends Phaser.Scene {
// Pick random result
const result = candidates[Math.floor(Math.random() * candidates.length)];
// Complete stage 6 tutorial when the player clicks Fuse
this._completeTutorialStage6();
this._animateCardsIntoMachine(() => {
this._playFusionAnimation(() => {
// Mutate save
@ -1100,6 +1114,222 @@ export class FusionScene extends Phaser.Scene {
});
}
// ── Tutorial ──────────────────────────────────────────────────────────────
_clearTutorialOverlay() {
for (const el of this._tutorialOverlayElements) {
if (el && el.scene) el.destroy();
}
this._tutorialOverlayElements = [];
}
_showTutorialPhase1() {
if (TutorialManager.isStageComplete(this.save, TutorialManager.STAGES.FUSION_INTRO)) return;
const stacks = this._computeStacks();
const fusibleStacks = stacks.filter(s => s.fusible);
if (fusibleStacks.length === 0) return;
const { width, height } = this.scale;
const DEPTH = 500;
this._tutorialPhase = 1;
// ── Targeted dimming approach ─────────────────────────────────────────────
// Instead of one full-screen interactive overlay (which blocks clicks on the
// stacks beneath it), we dim only the areas that should be inactive:
// 1. Everything to the RIGHT of the stacks panel (video, preview, deck)
// 2. Each individual NON-fusible stack
// Fusible stacks are left fully visible and clickable with no overlay on them.
const DIM = 0x000000;
const DIM_ALPHA = 0.80;
// Right-side dimmer (video + preview + deck panels)
const rightX = STACKS_LEFT + STACKS_W;
const rightW = width - rightX;
const rightDim = this.add.rectangle(
rightX + rightW / 2, height / 2, rightW, height, DIM, DIM_ALPHA
).setDepth(DEPTH).setInteractive(); // interactive = blocks clicks on right panel
this._tutorialOverlayElements.push(rightDim);
// Header/bottom-bar dimmers (above and below the stacks area)
const topDim = this.add.rectangle(
STACKS_LEFT + STACKS_W / 2, STACKS_TOP / 2,
STACKS_W, STACKS_TOP, DIM, DIM_ALPHA
).setDepth(DEPTH).setInteractive();
this._tutorialOverlayElements.push(topDim);
const bottomBarTop = STACKS_TOP + STACKS_H;
const bottomDim = this.add.rectangle(
STACKS_LEFT + STACKS_W / 2, bottomBarTop + (height - bottomBarTop) / 2,
STACKS_W, height - bottomBarTop, DIM, DIM_ALPHA
).setDepth(DEPTH).setInteractive();
this._tutorialOverlayElements.push(bottomDim);
// Dim each NON-fusible stack individually
const gridW = COLS * CELL_W;
const padX = (STACKS_W - gridW) / 2;
stacks.forEach((stack, idx) => {
const col = idx % COLS;
const row = Math.floor(idx / COLS);
const cx = STACKS_LEFT + padX + col * CELL_W + CARD_W / 2;
const cy = STACKS_TOP + 10 + row * CELL_H + CARD_H / 2;
if (!stack.fusible) {
// Dark cover over this non-fusible card — also blocks its click
const dim = this.add.rectangle(cx, cy, CARD_W + 4, CARD_H + 4, DIM, 0.72)
.setDepth(DEPTH).setInteractive();
this._tutorialOverlayElements.push(dim);
} else {
// Pulsing gold border around each fusible card (added above stacks at DEPTH+1,
// purely decorative — the card's own click handler fires normally)
const border = this.add.rectangle(cx, cy, CARD_W + 12, CARD_H + 12)
.setStrokeStyle(3, 0xffd700)
.setFillStyle(0x000000, 0)
.setDepth(DEPTH + 1);
this._tutorialOverlayElements.push(border);
this.tweens.add({
targets: border,
strokeAlpha: { from: 0.4, to: 1 },
duration: 700,
yoyo: true,
repeat: -1,
ease: 'Sine.InOut'
});
// Pulsing arrow above the card
const arrowY = cy - CARD_H / 2 - 52;
const arrow = this.add.text(cx, arrowY, '\u25BC', {
fontSize: '36px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 1);
this._tutorialOverlayElements.push(arrow);
this.tweens.add({
targets: arrow,
y: arrowY + 14,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
}
});
// Instructional text — sits in the right panel area (already dimmed, no interaction needed)
const infoText = this.add.text(rightX + rightW / 2, height / 2, [
'These cards can be fused!',
'Select one to see what it can become.'
].join('\n'), {
fontSize: '24px', color: '#ffffff', fontFamily: 'Audiowide',
align: 'center', lineSpacing: 8
}).setOrigin(0.5).setDepth(DEPTH + 1);
this._tutorialOverlayElements.push(infoText);
}
_showTutorialPhase2() {
// Called after the player selects a fusible stack during phase 1
if (this._tutorialPhase !== 1) return;
this._clearTutorialOverlay();
this._tutorialPhase = 2;
const { width, height } = this.scale;
const DEPTH = 500;
// Dark overlay
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.80)
.setDepth(DEPTH)
.setInteractive();
this._tutorialOverlayElements.push(overlay);
// Expose: forge video area
const videoHoleX = VIDEO_CX;
const videoHoleY = VIDEO_CY;
const videoHoleW = VIDEO_W + 20;
const videoHoleH = VIDEO_H + 20;
const videoBorder = this.add.rectangle(videoHoleX, videoHoleY, videoHoleW, videoHoleH)
.setStrokeStyle(2, 0x553300)
.setFillStyle(0x000000, 0)
.setDepth(DEPTH + 1);
this._tutorialOverlayElements.push(videoBorder);
// Bring video above overlay
if (this._videoRect) this._videoRect.setDepth(DEPTH + 2);
if (this._forgeVideo) this._forgeVideo.setDepth(DEPTH + 2);
if (this._forgeActivateVideo) this._forgeActivateVideo.setDepth(DEPTH + 2);
// Expose: preview title and cards
if (this._previewTitle) this._previewTitle.setDepth(DEPTH + 2);
if (this._previewContainer) this._previewContainer.setDepth(DEPTH + 2);
// Expose: selected stack
if (this._selectedContainer) this._selectedContainer.setDepth(DEPTH + 2);
// Expose: fuse button — highlight it
if (this._fuseBtnBg) {
this._fuseBtnBg.setDepth(DEPTH + 2);
this.tweens.add({
targets: this._fuseBtnBg,
strokeAlpha: { from: 0.4, to: 1 },
duration: 700,
yoyo: true,
repeat: -1,
ease: 'Sine.InOut'
});
}
if (this._fuseBtnText) this._fuseBtnText.setDepth(DEPTH + 2);
if (this._fuseInfoText) this._fuseInfoText.setDepth(DEPTH + 2);
// Instructional text and arrow to the LEFT of the Fuse button (center x=1060, left edge x=930)
const resultRarity = this._selectedStack?.recipe?.result_rarity?.toUpperCase() || 'higher rarity';
const cardName = this._selectedStack?.cardDef?.name || 'card';
// Text block centered in the space left of the fuse button
const textCx = 470;
const infoText = this.add.text(textCx, 970, [
`Clicking FUSE will consume 3× ${cardName} and forge`,
`a random ${resultRarity} card from the same faction.`,
'The result is random — may the forge be with you!'
].join('\n'), {
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide',
align: 'center', lineSpacing: 8,
wordWrap: { width: 700 }
}).setOrigin(0.5).setDepth(DEPTH + 2);
this._tutorialOverlayElements.push(infoText);
// Right-pointing arrow between text and button, pulsing toward the button
const arrowX = 910;
const arrow = this.add.text(arrowX, 970, '\u25BA', {
fontSize: '42px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 2);
this._tutorialOverlayElements.push(arrow);
this.tweens.add({
targets: arrow,
x: arrowX + 14,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
}
_completeTutorialStage6() {
if (TutorialManager.isStageComplete(this.save, TutorialManager.STAGES.FUSION_INTRO)) return;
this._clearTutorialOverlay();
this._tutorialPhase = null;
TutorialManager.completeStage(this.save, TutorialManager.STAGES.FUSION_INTRO);
// Reset depths elevated for phase 2
if (this._videoRect) this._videoRect.setDepth(0);
if (this._forgeVideo) this._forgeVideo.setDepth(0);
if (this._forgeActivateVideo) this._forgeActivateVideo.setDepth(0);
if (this._previewTitle) this._previewTitle.setDepth(0);
if (this._previewContainer) this._previewContainer.setDepth(0);
if (this._fuseBtnBg) this._fuseBtnBg.setDepth(0);
if (this._fuseBtnText) this._fuseBtnText.setDepth(0);
if (this._fuseInfoText) this._fuseInfoText.setDepth(0);
}
// ── Messages ──────────────────────────────────────────────────────────────
_showMsg(msg, color) {

View File

@ -140,8 +140,24 @@ export class MainMenuScene extends Phaser.Scene {
_showTutorial() {
const save = this.registry.get('save');
if (TutorialManager.isStageComplete(save, TutorialManager.STAGES.CAMPAIGN_INTRO)) return;
if (!TutorialManager.isStageComplete(save, TutorialManager.STAGES.CAMPAIGN_INTRO)) {
this._showCampaignIntroTutorial(save);
} else if (
save.gold > 400 &&
!TutorialManager.isStageComplete(save, TutorialManager.STAGES.STORE_INTRO)
) {
this._showStoreIntroTutorial(save);
} else if (
TutorialManager.isStageComplete(save, TutorialManager.STAGES.STORE_INTRO) &&
!TutorialManager.isStageComplete(save, TutorialManager.STAGES.FUSION_INTRO) &&
TutorialManager.hasFusibleCards(save)
) {
this._showFusionIntroTutorial(save);
}
}
_showCampaignIntroTutorial(save) {
const { width, height } = this.scale;
const DEPTH = 1000;
@ -201,6 +217,67 @@ export class MainMenuScene extends Phaser.Scene {
});
}
_showStoreIntroTutorial(save) {
const { width, height } = this.scale;
const DEPTH = 1000;
// Store button is index 4 in the button list (startY=310, spacing=90)
const btnX = width / 2;
const btnY = 310 + 4 * 90; // 670
// Dark overlay — blocks all clicks beneath
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78)
.setDepth(DEPTH)
.setInteractive();
// Duplicate Store 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, 'Store', {
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, [
'Every battle — win or lose — earns you Gold.',
'Gold is spent in the Store to buy card packs',
'and strengthen your deck.',
'Click Store to spend your hard-earned Gold!'
].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('StoreScene');
});
}
_makeButton(x, y, label, callback) {
const bg = this.add.rectangle(x, y, 400, 65, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
@ -332,6 +409,67 @@ export class MainMenuScene extends Phaser.Scene {
});
}
_showFusionIntroTutorial(save) {
const { width, height } = this.scale;
const DEPTH = 1000;
// Fusion Lab is index 5 in the button list (startY=310, spacing=90)
const btnX = width / 2;
const btnY = 310 + 5 * 90; // 760
// Dark overlay — blocks all clicks beneath
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.78)
.setDepth(DEPTH)
.setInteractive();
// Duplicate Fusion Lab 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, 'Fusion Lab', {
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, [
'You have enough cards to fuse!',
'In the Fusion Lab, combine 3 copies of any card',
'to forge a random card of the next rarity in the same faction.',
'Click Fusion Lab to try it out!'
].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('FusionScene');
});
}
_makeFullscreenButton(x, y) {
const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen';

View File

@ -1,5 +1,6 @@
import { SaveManager } from '../managers/SaveManager.js';
import { CardObject } from '../objects/CardObject.js';
import { TutorialManager } from '../managers/TutorialManager.js';
const FACTION_COLORS = {
imperial: 0x2244aa,
@ -99,6 +100,9 @@ export class StoreScene extends Phaser.Scene {
// Back button
this._makeBackButton();
// Stage 5 tutorial overlay (shown after packs are rendered)
this._showTutorial();
}
// ── Pack Grid ─────────────────────────────────────────────────────────────
@ -533,4 +537,119 @@ export class StoreScene extends Phaser.Scene {
this.scene.start('MainMenuScene');
});
}
// ── Tutorial ──────────────────────────────────────────────────────────────
_showTutorial() {
if (TutorialManager.isStageComplete(this.save, TutorialManager.STAGES.STORE_INTRO)) return;
if (this.save.gold <= 400) return;
const { width, height } = this.scale;
const DEPTH = 1000;
// Find the imperial pack and calculate its position (same formula as _renderAllPacks)
const packs = this.packManager.getAllPacks();
const imperialIndex = packs.findIndex(p => p.faction === 'imperial');
if (imperialIndex === -1) return;
const packW = 320;
const gap = 30;
const totalW = packs.length * packW + (packs.length - 1) * gap;
const startX = width / 2 - totalW / 2 + packW / 2;
const packX = startX + imperialIndex * (packW + gap);
const packY = 400;
// Highlight rect that reveals just the imperial pack (300x440 card + some padding)
const highlightPad = 18;
const hlX = packX;
const hlY = packY;
const hlW = 300 + highlightPad * 2;
const hlH = 440 + highlightPad * 2;
// Dark overlay covering everything
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.80)
.setDepth(DEPTH)
.setInteractive(); // blocks clicks underneath
// Cut-out effect: four dark rects around the highlight area
const top = this.add.rectangle(width / 2, (hlY - hlH / 2) / 2, width, hlY - hlH / 2, 0x000000, 0.80).setDepth(DEPTH + 1);
const bottom = this.add.rectangle(width / 2, hlY + hlH / 2 + (height - (hlY + hlH / 2)) / 2, width, height - (hlY + hlH / 2), 0x000000, 0.80).setDepth(DEPTH + 1);
const left = this.add.rectangle((hlX - hlW / 2) / 2, hlY, hlX - hlW / 2, hlH, 0x000000, 0.80).setDepth(DEPTH + 1);
const right = this.add.rectangle(hlX + hlW / 2 + (width - (hlX + hlW / 2)) / 2, hlY, width - (hlX + hlW / 2), hlH, 0x000000, 0.80).setDepth(DEPTH + 1);
// Bright border around the highlighted pack
const highlight = this.add.rectangle(hlX, hlY, hlW, hlH)
.setStrokeStyle(3, 0xffd700)
.setFillStyle(0x000000, 0)
.setDepth(DEPTH + 2);
// Pulsing glow on highlight border
this.tweens.add({
targets: highlight,
strokeAlpha: { from: 0.4, to: 1 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.InOut'
});
// Buy Pack button position: y = packY + 170
const buyBtnX = packX;
const buyBtnY = packY + 170;
// Duplicate Buy Pack button on top of overlay
const buyBtnBg = this.add.rectangle(buyBtnX, buyBtnY, 240, 55, 0x224422)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x44aa44)
.setDepth(DEPTH + 3);
const buyBtnText = this.add.text(buyBtnX, buyBtnY, 'Buy Pack', {
fontSize: '20px', color: '#44ff44', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(DEPTH + 3);
buyBtnBg.on('pointerover', () => buyBtnBg.setFillStyle(0x336633));
buyBtnBg.on('pointerout', () => buyBtnBg.setFillStyle(0x224422));
// Pulsing arrow above Buy Pack button
const arrow = this.add.text(buyBtnX, buyBtnY - 60, '\u25BC', {
fontSize: '42px', color: '#ffd700'
}).setOrigin(0.5).setDepth(DEPTH + 3);
this.tweens.add({
targets: arrow,
y: buyBtnY - 44,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Instructional text below the pack
const textY = hlY + hlH / 2 + 30;
const infoText = this.add.text(width / 2, textY, [
'Each faction has a Standard Pack that costs 400 Gold.',
'More factions will unlock as you defeat them in the Campaign.',
'Click Buy Pack to open your first pack!'
].join('\n'), {
fontSize: '22px',
color: '#ffffff',
fontFamily: 'Audiowide',
align: 'center',
lineSpacing: 8,
wordWrap: { width: 1400 }
}).setOrigin(0.5, 0).setDepth(DEPTH + 3);
const tutorialElements = [overlay, top, bottom, left, right, highlight, buyBtnBg, buyBtnText, arrow, infoText];
buyBtnBg.on('pointerdown', () => {
// Complete the tutorial stage
TutorialManager.completeStage(this.save, TutorialManager.STAGES.STORE_INTRO);
// Destroy tutorial overlay
tutorialElements.forEach(el => { if (el && el.scene) el.destroy(); });
// Trigger actual pack purchase
const imperialPack = packs[imperialIndex];
this._buyPack(imperialPack);
});
}
}