import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js'; import { auth } from '../services/auth.js'; import { Button } from '../ui/Button.js'; import { Plaque } from '../ui/Plaque.js'; import { addFullscreenButton } from '../ui/FullscreenButton.js'; import { playMenuMusic } from '../ui/MenuMusic.js'; export default class LandingScene extends Phaser.Scene { constructor() { super('Landing'); } create() { playMenuMusic(); const cx = GAME_WIDTH / 2; this.add.image(cx, GAME_HEIGHT / 2, 'bg-menu').setDisplaySize(GAME_WIDTH, GAME_HEIGHT); const logo = this.add.image(cx, 290, 'main-title').setOrigin(0.5, 0.5).setAlpha(0); logo.postFX.addShadow(3, 6, 0.005, 2, 0x000000, 10, 0.75); this._logo = logo; this._transitioning = false; this.tweens.add({ targets: logo, alpha: 1, y: 260, duration: 900, ease: 'Power2', onComplete: () => { this.tweens.add({ targets: logo, scaleX: 1.025, scaleY: 1.025, duration: 2200, yoyo: true, repeat: -1, ease: 'Sine.easeInOut', }); }, }); this.renderButtons(); addFullscreenButton(this); if (this._authUnsub) this._authUnsub(); this._authUnsub = auth.subscribe(() => { if (!this.scene.isActive('Landing')) return; this.children.removeAll(); this.create(); }); this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { if (this._authUnsub) { this._authUnsub(); this._authUnsub = null; } }); } renderButtons() { const cx = GAME_WIDTH / 2; const user = auth.user; this._avatar = null; const avatarR = 28; const avatarGap = 18; const pad = { x: 32, y: 14 }; const y = 630; const name = user?.displayName ?? user?.username ?? 'Player'; const welcomeText = this.add.text(0, y, `Welcome, ${name}`, { fontFamily: 'Righteous', fontSize: '36px', color: COLORS.accentHex, }).setOrigin(0.5).setDepth(1); this._welcomeText = welcomeText; welcomeText.setShadow(0, 3, 'rgba(0,0,0,0.8)', 5); const hasAvatar = !!user?.avatarPath; const totalW = hasAvatar ? avatarR * 2 + avatarGap + welcomeText.width : welcomeText.width; const groupLeft = cx - totalW / 2; welcomeText.setX(hasAvatar ? groupLeft + avatarR * 2 + avatarGap + welcomeText.width / 2 : cx); this._welcomeBg = new Plaque(this, totalW + pad.x * 2, welcomeText.height + pad.y * 2, { radius: 16 }) .setPosition(cx, y); if (hasAvatar) { const avatarCx = groupLeft + avatarR; const key = `landing-avatar-${user.id}`; (async () => { try { if (!this.textures.exists(key)) { await new Promise((resolve) => { this.load.image(key, user.avatarPath); this.load.once('complete', resolve); this.load.start(); }); } if (this._transitioning || !this.scene.isActive('Landing')) return; const maskG = this.make.graphics({ x: 0, y: 0, add: false }); maskG.fillStyle(0xffffff); maskG.fillCircle(avatarCx, y, avatarR); this._avatar = this.add.image(avatarCx, y, key) .setDisplaySize(avatarR * 2, avatarR * 2) .setMask(maskG.createGeometryMask()) .setDepth(1); } catch { /* no avatar shown */ } })(); } this.playBtn = new Button(this, cx, 810, 'Play', () => this.goToGameMenu(), { width: 480 }); this.profileBtn = new Button(this, cx, 890, 'Profile', () => this.scene.start('Profile'), { width: 480 }); } goToGameMenu() { if (this._transitioning) return; this._transitioning = true; this.playBtn.disableInteractive(); this.profileBtn.disableInteractive(); // Fade the landing buttons out while the logo zooms past the camera. const fadeOut = [this.playBtn, this.profileBtn, this._welcomeText, this._welcomeBg].filter(Boolean); if (this._avatar) fadeOut.push(this._avatar); this.tweens.add({ targets: fadeOut, alpha: 0, duration: 350, ease: 'Power2.easeOut', }); if (!this._logo) { this.scene.start('GameMenu'); return; } this.tweens.killTweensOf(this._logo); this.tweens.add({ targets: this._logo, x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2, scaleX: 2.4, scaleY: 2.4, alpha: 0, duration: 700, ease: 'Cubic.easeIn', onComplete: () => this.scene.start('GameMenu'), }); } }