733 lines
26 KiB
JavaScript
733 lines
26 KiB
JavaScript
import { SaveManager } from '../managers/SaveManager.js';
|
|
import { TutorialManager } from '../managers/TutorialManager.js';
|
|
|
|
export class MainMenuScene extends Phaser.Scene {
|
|
constructor() { super('MainMenuScene'); }
|
|
|
|
preload() {
|
|
this.load.video('menu_bg', 'assets/video/tyrants-edge-menu.mp4');
|
|
}
|
|
|
|
create() {
|
|
const save = this.registry.get('save');
|
|
const { width, height } = this.scale;
|
|
|
|
// Start music on first visit; resume it on return from other scenes
|
|
let menuMusic = this.registry.get('music_main_menu');
|
|
if (!menuMusic) {
|
|
menuMusic = this.sound.add('music_main_menu', { loop: true, volume: 0.5 });
|
|
menuMusic.play();
|
|
this.registry.set('music_main_menu', menuMusic);
|
|
} else if (!menuMusic.isPlaying) {
|
|
menuMusic.play();
|
|
}
|
|
|
|
// ── Video background ──────────────────────────────────────────────────────
|
|
const video = this.add.video(width / 2, height / 2, 'menu_bg');
|
|
video.setMute(true);
|
|
// setDisplaySize reads frame.realWidth which is null until the first frame
|
|
// is decoded — defer sizing to the 'created' event.
|
|
video.on('created', () => {
|
|
if (video.scene) video.setDisplaySize(width, height);
|
|
});
|
|
video.play(true); // true = loop
|
|
|
|
// ── VHS / retro overlay effects ───────────────────────────────────────────
|
|
|
|
// 1. Dark tint so UI stays readable over whatever the video shows
|
|
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.42);
|
|
|
|
// 2. Scanlines — one dark horizontal pixel every 3 rows, baked into graphics
|
|
const scanlines = this.add.graphics();
|
|
for (let y = 0; y < height; y += 3) {
|
|
scanlines.fillStyle(0x000000, 0.22);
|
|
scanlines.fillRect(0, y, width, 1);
|
|
}
|
|
|
|
// 3. Vignette — stepped gradient darkening from each edge inward
|
|
const vig = this.add.graphics();
|
|
const steps = 12;
|
|
const vigSize = 180;
|
|
for (let i = 0; i < steps; i++) {
|
|
const a = (1 - i / steps) * 0.65;
|
|
const t = Math.round((vigSize / steps) * i);
|
|
const s = Math.ceil(vigSize / steps);
|
|
vig.fillStyle(0x000000, a);
|
|
vig.fillRect(t, 0, s, height); // left
|
|
vig.fillRect(width - t - s, 0, s, height); // right
|
|
vig.fillRect(0, t, width, s); // top
|
|
vig.fillRect(0, height - t - s, width, s); // bottom
|
|
}
|
|
|
|
// 4. Rolling VHS tracking bar — thin translucent band drifts down the screen
|
|
const scanBar = this.add.rectangle(width / 2, -30, width, 60, 0xffffff, 0.045);
|
|
this.tweens.add({
|
|
targets: scanBar,
|
|
y: height + 30,
|
|
duration: 7500,
|
|
repeat: -1,
|
|
ease: 'Linear'
|
|
});
|
|
|
|
// 5. Occasional screen flicker — random brief brightness pulse
|
|
const scheduleFlicker = () => {
|
|
this.time.delayedCall(Phaser.Math.Between(2500, 7000), () => {
|
|
if (!this.scene.isActive('MainMenuScene')) return;
|
|
const flash = this.add.rectangle(width / 2, height / 2, width, height, 0xffffff, 0.06);
|
|
this.time.delayedCall(60, () => {
|
|
if (flash.scene) flash.destroy();
|
|
scheduleFlicker();
|
|
});
|
|
});
|
|
};
|
|
scheduleFlicker();
|
|
|
|
// Title
|
|
this.add.text(width / 2, 120, "TYRANT'S EDGE", {
|
|
fontSize: '72px',
|
|
color: '#d4af37',
|
|
stroke: '#000000',
|
|
strokeThickness: 6,
|
|
fontFamily: 'RaiderCrusader'
|
|
}).setOrigin(0.5);
|
|
|
|
this.add.text(width / 2, 190, 'A Collectible Card Game by Brian Fertig', {
|
|
fontSize: '24px', color: '#888888', fontFamily: 'Audiowide'
|
|
}).setOrigin(0.5);
|
|
|
|
// Level & Gold display
|
|
this.add.text(width - 30, 30, `Level: ${save.level || 1}`, {
|
|
fontSize: '24px', color: '#88aaff', fontFamily: 'Audiowide'
|
|
}).setOrigin(1, 0);
|
|
|
|
this.goldText = this.add.text(width - 30, 60, `Gold: ${save.gold}`, {
|
|
fontSize: '24px', color: '#ffd700', fontFamily: 'Audiowide'
|
|
}).setOrigin(1, 0);
|
|
|
|
// Version
|
|
this.add.text(20, height - 20, 'v1.0', {
|
|
fontSize: '14px', color: '#444444', fontFamily: 'Audiowide'
|
|
}).setOrigin(0, 1);
|
|
|
|
const buttons = [
|
|
{ label: 'Campaign', scene: 'CampaignSelectScene' },
|
|
{ label: 'Skirmish', scene: 'SkirmishSetupScene' },
|
|
{ label: 'Deck Builder', scene: 'DeckBuilderScene' },
|
|
{ label: 'Collection', scene: 'CollectionScene' },
|
|
{ label: 'Store', scene: 'StoreScene' },
|
|
{ label: 'Fusion Lab', scene: 'FusionScene' }
|
|
];
|
|
|
|
const startY = 310;
|
|
const spacing = 90;
|
|
|
|
buttons.forEach((btn, i) => {
|
|
this._makeButton(width / 2, startY + i * spacing, btn.label, () => {
|
|
this.scene.start(btn.scene, btn.data || {});
|
|
});
|
|
});
|
|
|
|
// Fullscreen toggle — utility button, visually distinct
|
|
const fsY = startY + buttons.length * spacing + 20;
|
|
this._makeFullscreenButton(width / 2, fsY);
|
|
|
|
// Save / Restore — subtle utility buttons, upper-left
|
|
this._makeSaveRestoreButtons();
|
|
|
|
// Tutorial overlay (if applicable)
|
|
this._showTutorial();
|
|
}
|
|
|
|
_showTutorial() {
|
|
const save = this.registry.get('save');
|
|
|
|
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);
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
_showCampaignIntroTutorial(save) {
|
|
const { width, height } = this.scale;
|
|
const DEPTH = 1000;
|
|
|
|
// Dark overlay — blocks all clicks beneath
|
|
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.75)
|
|
.setDepth(DEPTH)
|
|
.setInteractive();
|
|
|
|
// Duplicate Campaign button on top of overlay
|
|
const btnX = width / 2;
|
|
const btnY = 310;
|
|
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, 'Campaign', {
|
|
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));
|
|
|
|
// Bobbing 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 are starting the game as Warlord Voss.',
|
|
'Your first mission: battle The Raiders!',
|
|
'Click Campaign to begin.'
|
|
].join('\n'), {
|
|
fontSize: '22px',
|
|
color: '#ffffff',
|
|
fontFamily: 'Audiowide',
|
|
align: 'center',
|
|
lineSpacing: 8
|
|
}).setOrigin(0.5, 0).setDepth(DEPTH + 1);
|
|
|
|
// Click — complete stage, clean up, navigate
|
|
const tutorialElements = [overlay, btnBg, btnText, arrow, infoText];
|
|
btnBg.on('pointerdown', () => {
|
|
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
|
TutorialManager.completeStage(save, TutorialManager.STAGES.CAMPAIGN_INTRO);
|
|
tutorialElements.forEach(el => el.destroy());
|
|
this.scene.start('CampaignSelectScene');
|
|
});
|
|
}
|
|
|
|
_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 })
|
|
.setStrokeStyle(2, 0x4488ff);
|
|
|
|
const text = this.add.text(x, y, label, {
|
|
fontSize: '28px', color: '#ffffff', fontFamily: 'Audiowide'
|
|
}).setOrigin(0.5);
|
|
|
|
bg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); bg.setFillStyle(0x2a5a8c); });
|
|
bg.on('pointerout', () => bg.setFillStyle(0x1a3a5c));
|
|
bg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); callback(); });
|
|
|
|
return { bg, text };
|
|
}
|
|
|
|
// ── Save file obfuscation ─────────────────────────────────────────────────
|
|
// XOR-cipher + Base64. Not cryptographically secure, but the exported file
|
|
// contains no readable JSON, making casual hand-editing impractical.
|
|
// The key lives in client code so a determined reverse-engineer could decode
|
|
// it — the goal is to deter accidental or casual cheating, not prevent it.
|
|
|
|
_encodeData(obj) {
|
|
const K = 'TE::v1::TyrantsEdge::SaveKey';
|
|
const s = JSON.stringify(obj);
|
|
let r = '';
|
|
for (let i = 0; i < s.length; i++) {
|
|
r += String.fromCharCode(s.charCodeAt(i) ^ K.charCodeAt(i % K.length));
|
|
}
|
|
return btoa(r);
|
|
}
|
|
|
|
_decodeData(str) {
|
|
// Try obfuscated format first, fall back to plain JSON for legacy files.
|
|
try {
|
|
const K = 'TE::v1::TyrantsEdge::SaveKey';
|
|
const s = atob(str.trim());
|
|
let r = '';
|
|
for (let i = 0; i < s.length; i++) {
|
|
r += String.fromCharCode(s.charCodeAt(i) ^ K.charCodeAt(i % K.length));
|
|
}
|
|
return JSON.parse(r);
|
|
} catch {
|
|
return JSON.parse(str); // plain JSON (old save files)
|
|
}
|
|
}
|
|
|
|
_makeSaveRestoreButtons() {
|
|
const BW = 250, BH = 34, x = 20 + BW / 2;
|
|
|
|
// ── Save to Drive ──
|
|
const saveBg = this.add.rectangle(x, 25, BW, BH, 0x000000, 0.55)
|
|
.setInteractive({ useHandCursor: true })
|
|
.setStrokeStyle(1, 0x336633);
|
|
this.add.text(x, 25, '💾 Save Game to Your Drive', {
|
|
fontSize: '14px', color: '#669966', fontFamily: 'Audiowide'
|
|
}).setOrigin(0.5);
|
|
|
|
saveBg.on('pointerover', () => saveBg.setStrokeStyle(1, 0x55aa55));
|
|
saveBg.on('pointerout', () => saveBg.setStrokeStyle(1, 0x336633));
|
|
saveBg.on('pointerdown', () => {
|
|
try {
|
|
const data = this.registry.get('save');
|
|
const blob = new Blob([this._encodeData(data)], { type: 'application/octet-stream' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'tyrants-edge-save.te';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
this._showStatusMsg('Save file downloaded!', '#66bb66');
|
|
} catch (e) {
|
|
this._showStatusMsg('Save failed.', '#bb4444');
|
|
}
|
|
});
|
|
|
|
// ── Restore from File ──
|
|
const restoreBg = this.add.rectangle(x, 65, BW, BH, 0x000000, 0.55)
|
|
.setInteractive({ useHandCursor: true })
|
|
.setStrokeStyle(1, 0x334466);
|
|
this.add.text(x, 65, '📂 Restore Game from File', {
|
|
fontSize: '14px', color: '#6688aa', fontFamily: 'Audiowide'
|
|
}).setOrigin(0.5);
|
|
|
|
restoreBg.on('pointerover', () => restoreBg.setStrokeStyle(1, 0x5577bb));
|
|
restoreBg.on('pointerout', () => restoreBg.setStrokeStyle(1, 0x334466));
|
|
restoreBg.on('pointerdown', () => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.te,.json,application/octet-stream,application/json';
|
|
input.style.display = 'none';
|
|
document.body.appendChild(input);
|
|
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
document.body.removeChild(input);
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (evt) => {
|
|
try {
|
|
const restored = this._decodeData(evt.target.result);
|
|
// Basic sanity check
|
|
if (typeof restored !== 'object' || !restored.collection) throw new Error();
|
|
SaveManager.save(restored);
|
|
this.registry.set('save', restored);
|
|
this._showStatusMsg('Restored! Reloading...', '#66bb66');
|
|
this.time.delayedCall(1200, () => this.scene.restart());
|
|
} catch {
|
|
this._showStatusMsg('Invalid save file.', '#bb4444');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
|
|
input.click();
|
|
});
|
|
}
|
|
|
|
_showStatusMsg(msg, color) {
|
|
if (this._statusMsg) this._statusMsg.destroy();
|
|
this._statusMsg = this.add.text(145, 95, msg, {
|
|
fontSize: '13px', color, fontFamily: 'Audiowide'
|
|
}).setOrigin(0.5, 0);
|
|
this.time.delayedCall(3000, () => {
|
|
if (this._statusMsg) { this._statusMsg.destroy(); this._statusMsg = null; }
|
|
});
|
|
}
|
|
|
|
_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');
|
|
});
|
|
}
|
|
|
|
_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';
|
|
|
|
const bg = this.add.rectangle(x, y, 320, 50, 0x1a2a1a)
|
|
.setInteractive({ useHandCursor: true })
|
|
.setStrokeStyle(2, 0x448844);
|
|
|
|
const text = this.add.text(x, y, getLabel(), {
|
|
fontSize: '20px', color: '#88cc88', fontFamily: 'Audiowide'
|
|
}).setOrigin(0.5);
|
|
|
|
bg.on('pointerover', () => { this.sound.play('sfx_menu_hover', { volume: 0.5 }); bg.setFillStyle(0x2a3f2a); });
|
|
bg.on('pointerout', () => bg.setFillStyle(0x1a2a1a));
|
|
bg.on('pointerdown', () => {
|
|
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
|
if (this.scale.isFullscreen) {
|
|
this.scale.stopFullscreen();
|
|
} else {
|
|
this.scale.startFullscreen();
|
|
}
|
|
// Update label after a short delay to let the state change
|
|
this.time.delayedCall(100, () => text.setText(getLabel()));
|
|
});
|
|
}
|
|
}
|