/** * Hauptmenü mit Propaganda-Laufband. Beim allerersten Start wird zuerst der * Disclaimer angezeigt; das Menü ist erst danach bedienbar. * @module scenes/MenuScene */ import { GAME } from '../config.js'; import { MENU, PROPAGANDA, TAGLINE, TITLE } from '../content/texts.js'; import { TEXTURE_KEYS } from '../systems/Placeholders.js'; import { MenuList } from '../ui/MenuList.js'; import { textStyle, UI, uiStyle } from '../ui/theme.js'; /** Geschwindigkeit des Laufbands in px/s. */ const TICKER_SPEED = 90; /** Anzahl Scheinwerferkegel im Hintergrund. */ const SEARCHLIGHTS = 3; export class MenuScene extends Phaser.Scene { constructor() { super('Menu'); } create() { const { width, height } = this.scale; const services = this.registry.get('services'); this.input.setDefaultCursor('default'); this.drawBackground(width, height); this.add.image(width / 2, 150, TEXTURE_KEYS.emblem).setDisplaySize(150, 150); this.add .text(width / 2, 262, TITLE, textStyle({ fontSize: '68px', color: UI.colors.gold, strokeThickness: 10, stroke: '#7a0a12' })) .setOrigin(0.5); this.add.text(width / 2, 318, TAGLINE, textStyle({ fontSize: '24px' })).setOrigin(0.5); this.menu = new MenuList(this, width / 2, 372, [ { label: MENU.start, onSelect: () => this.startMission() }, { label: MENU.highscores, onSelect: () => this.scene.start('Highscores') }, { label: MENU.controls, onSelect: () => this.scene.start('Controls') }, { label: MENU.disclaimer, onSelect: () => this.openDisclaimer(false) }, { label: MENU.credits, onSelect: () => this.scene.start('Credits') }, ], { spacing: 54 }); this.add.text(width / 2, height - 62, MENU.hint, uiStyle({ fontSize: '16px', color: UI.colors.muted })).setOrigin(0.5); this.createTicker(width, height); // Reine Touch-Geräte (Handy, Tablet) haben weder WASD noch Maus – lieber gleich sagen. if (globalThis.matchMedia?.('(pointer: coarse)').matches && !globalThis.matchMedia?.('(any-pointer: fine)').matches) { this.add.text(width / 2, height - 92, MENU.touchNotice, uiStyle({ fontSize: '18px', color: UI.colors.danger })).setOrigin(0.5); } // Ton darf erst nach einer Nutzeraktion starten (Browser-Richtlinie); die Musik // wird vorgemerkt und beginnt mit dem ersten Klick oder Tastendruck. services.audio.startMusic(); this.input.once('pointerdown', () => services.audio.unlock()); this.input.keyboard.once('keydown', () => services.audio.unlock()); if (!services.settings.isDisclaimerAccepted()) { this.openDisclaimer(true); } } /** * @param {boolean} mandatory Beim ersten Start muss bestätigt werden. */ openDisclaimer(mandatory) { this.menu.setEnabled(false); this.scene.launch('Disclaimer', { mandatory }); this.scene.get('Disclaimer').events.once('disclaimer-closed', () => { this.menu.setEnabled(true); }); } startMission() { this.registry.get('services').audio.unlock(); this.scene.start('Briefing'); } drawBackground(width, height) { const bg = this.add.graphics(); bg.fillGradientStyle(0x0d1321, 0x0d1321, 0x2a0a10, 0x2a0a10, 1); bg.fillRect(0, 0, width, height); // Diagonale Streifen wie auf einem Propagandaplakat. bg.fillStyle(0xc1121f, 0.08); for (let x = -height; x < width; x += 90) { bg.fillTriangle(x, height, x + 45, height, x + height + 45, 0); } for (let i = 0; i < SEARCHLIGHTS; i += 1) { const beam = this.add.triangle(width * (0.2 + 0.3 * i), height + 20, 0, 0, -120, -height * 1.1, 120, -height * 1.1, 0xf2c14e, 0.06); beam.setOrigin(0, 0); this.tweens.add({ targets: beam, angle: { from: -25, to: 25 }, duration: 4200 + i * 900, yoyo: true, repeat: -1, ease: 'Sine.easeInOut', }); } } createTicker(width, height) { const bar = this.add.rectangle(0, height - 36, width, 36, 0xc1121f, 0.9).setOrigin(0, 0); bar.setDepth(GAME.depth.hud); const line = PROPAGANDA.join(' ★ '); this.ticker = this.add .text(width, height - 18, line, uiStyle({ fontSize: '19px', strokeThickness: 0 })) .setOrigin(0, 0.5) .setDepth(GAME.depth.hud + 1); } update(_time, delta) { if (!this.ticker) return; this.ticker.x -= (TICKER_SPEED * delta) / 1000; if (this.ticker.x < -this.ticker.width) this.ticker.x = this.scale.width; } }