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:
parent
5b206fda33
commit
0ea053dc44
|
|
@ -1,7 +1,7 @@
|
||||||
import { SaveManager } from './SaveManager.js';
|
import { SaveManager } from './SaveManager.js';
|
||||||
|
|
||||||
export class TutorialManager {
|
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) {
|
static isStageComplete(save, stageNum) {
|
||||||
return save.tutorialProgress?.completedStages?.includes(stageNum) ?? false;
|
return save.tutorialProgress?.completedStages?.includes(stageNum) ?? false;
|
||||||
|
|
@ -14,4 +14,21 @@ export class TutorialManager {
|
||||||
}
|
}
|
||||||
SaveManager.save(save);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { CardObject } from '../objects/CardObject.js';
|
import { CardObject } from '../objects/CardObject.js';
|
||||||
|
import { TutorialManager } from '../managers/TutorialManager.js';
|
||||||
|
|
||||||
// ── Amber CRT Palette ───────────────────────────────────────────────────────────
|
// ── Amber CRT Palette ───────────────────────────────────────────────────────────
|
||||||
const AMBER = '#ffaa33';
|
const AMBER = '#ffaa33';
|
||||||
|
|
@ -974,6 +975,15 @@ export class BattleResultScene extends Phaser.Scene {
|
||||||
if (e.stop) e.stop();
|
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) {
|
if (!this.missionData) {
|
||||||
// Skirmish → Main Menu
|
// Skirmish → Main Menu
|
||||||
this.scene.start('MainMenuScene');
|
this.scene.start('MainMenuScene');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { SaveManager } from '../managers/SaveManager.js';
|
import { SaveManager } from '../managers/SaveManager.js';
|
||||||
import { CardObject } from '../objects/CardObject.js';
|
import { CardObject } from '../objects/CardObject.js';
|
||||||
|
import { TutorialManager } from '../managers/TutorialManager.js';
|
||||||
|
|
||||||
const FUSION_RECIPES = {
|
const FUSION_RECIPES = {
|
||||||
common: { count: 3, result_rarity: 'rare' },
|
common: { count: 3, result_rarity: 'rare' },
|
||||||
|
|
@ -99,6 +100,11 @@ export class FusionScene extends Phaser.Scene {
|
||||||
this._setupScrolling();
|
this._setupScrolling();
|
||||||
this._generateParticleTexture();
|
this._generateParticleTexture();
|
||||||
this._renderStacks();
|
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 ─────────────────────────────────────────────────────────────────
|
// ── Header ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -543,6 +549,11 @@ export class FusionScene extends Phaser.Scene {
|
||||||
this._fuseInfoText.setVisible(true);
|
this._fuseInfoText.setVisible(true);
|
||||||
this._fuseBtnBg.setVisible(true);
|
this._fuseBtnBg.setVisible(true);
|
||||||
this._fuseBtnText.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() {
|
_deselectStack() {
|
||||||
|
|
@ -662,6 +673,9 @@ export class FusionScene extends Phaser.Scene {
|
||||||
// Pick random result
|
// Pick random result
|
||||||
const result = candidates[Math.floor(Math.random() * candidates.length)];
|
const result = candidates[Math.floor(Math.random() * candidates.length)];
|
||||||
|
|
||||||
|
// Complete stage 6 tutorial when the player clicks Fuse
|
||||||
|
this._completeTutorialStage6();
|
||||||
|
|
||||||
this._animateCardsIntoMachine(() => {
|
this._animateCardsIntoMachine(() => {
|
||||||
this._playFusionAnimation(() => {
|
this._playFusionAnimation(() => {
|
||||||
// Mutate save
|
// 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 ──────────────────────────────────────────────────────────────
|
// ── Messages ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_showMsg(msg, color) {
|
_showMsg(msg, color) {
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,24 @@ export class MainMenuScene extends Phaser.Scene {
|
||||||
|
|
||||||
_showTutorial() {
|
_showTutorial() {
|
||||||
const save = this.registry.get('save');
|
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 { width, height } = this.scale;
|
||||||
const DEPTH = 1000;
|
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) {
|
_makeButton(x, y, label, callback) {
|
||||||
const bg = this.add.rectangle(x, y, 400, 65, 0x1a3a5c)
|
const bg = this.add.rectangle(x, y, 400, 65, 0x1a3a5c)
|
||||||
.setInteractive({ useHandCursor: true })
|
.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) {
|
_makeFullscreenButton(x, y) {
|
||||||
const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen';
|
const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { SaveManager } from '../managers/SaveManager.js';
|
import { SaveManager } from '../managers/SaveManager.js';
|
||||||
import { CardObject } from '../objects/CardObject.js';
|
import { CardObject } from '../objects/CardObject.js';
|
||||||
|
import { TutorialManager } from '../managers/TutorialManager.js';
|
||||||
|
|
||||||
const FACTION_COLORS = {
|
const FACTION_COLORS = {
|
||||||
imperial: 0x2244aa,
|
imperial: 0x2244aa,
|
||||||
|
|
@ -99,6 +100,9 @@ export class StoreScene extends Phaser.Scene {
|
||||||
|
|
||||||
// Back button
|
// Back button
|
||||||
this._makeBackButton();
|
this._makeBackButton();
|
||||||
|
|
||||||
|
// Stage 5 tutorial overlay (shown after packs are rendered)
|
||||||
|
this._showTutorial();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pack Grid ─────────────────────────────────────────────────────────────
|
// ── Pack Grid ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -533,4 +537,119 @@ export class StoreScene extends Phaser.Scene {
|
||||||
this.scene.start('MainMenuScene');
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue