import Phaser from 'phaser'; import { GAME_WIDTH, GAME_HEIGHT, WORLD_SCALE } from '../config.js'; import { playMenuMusic } from '../util/music.js'; // bg_main.png (the poster art) already has the "MONSTERPLEX" wordmark baked // into its top-left corner, tilted a few degrees. logo.png is that same // wordmark as a standalone transparent-background graphic, meant to sit // exactly on top of the baked version so it can be the one animated // element - but logo.png's own art has a slightly different built-in tilt // than the baked version, so simply overlaying it flat left a visible // double edge. The constants below come from fitting a similarity // transform (rotation + uniform scale + translation) between the "M" and // "X" letter centroids measured in each image - the exact position/scale/ // rotation that maps logo.png's wordmark onto bg_main's - expressed as // fractions of bg_main.png's native 1672x941 pixels (position/width) or // plain degrees (rotation) so they scale correctly regardless of // WORLD_SCALE/canvas size. const LOGO_CENTER_X_FRAC = 0.25205; const LOGO_CENTER_Y_FRAC = 0.16541; const LOGO_WIDTH_FRAC = 0.75215; const LOGO_ASPECT = 752 / 1392; // logo.png's own native height/width // Negative = counter-clockwise (Phaser's angle is positive-clockwise) - // logo.png's own art is tilted ~2.4deg CCW already; the baked wordmark is // tilted ~7.6deg CCW, so this is the remaining ~5.2deg to close the gap. const LOGO_ROTATION_DEG = -8.22351; // Centered horizontally on the 5-icon row (DRIVE/JUMP/etc, itself centered // around x=878 of bg_main.png's 1672px width), vertically in the gap // between the jumping bus's tires (bottom edge ~y=700) and that icon row's // own top edge (~y=760). const PROMPT_X_FRAC = 0.45511; const PROMPT_Y_FRAC = 0.77077; export default class IntroScene extends Phaser.Scene { constructor() { super('Intro'); } create() { this.cameras.main.setBackgroundColor('#8fc7e8'); playMenuMusic(this); this.add.image(0, 0, 'bg_main').setOrigin(0, 0).setDisplaySize(GAME_WIDTH, GAME_HEIGHT); this._buildLogo(); this._buildPrompt(); // No auto-advance timer - this screen now waits indefinitely for the // player to actually press/tap. this._advance = () => this.scene.start('MainMenu'); this.input.keyboard.once('keydown', this._advance); this.input.once('pointerdown', this._advance); } // Layers logo.png exactly over the wordmark already painted into // bg_main.png, then breathes it (grow/shrink, looping) so it's the one // part of the poster that visibly moves. _buildLogo() { const logo = this.add.image(GAME_WIDTH * LOGO_CENTER_X_FRAC, GAME_HEIGHT * LOGO_CENTER_Y_FRAC, 'logo').setOrigin(0.5); logo.setAngle(LOGO_ROTATION_DEG); const targetWidth = GAME_WIDTH * LOGO_WIDTH_FRAC; logo.setDisplaySize(targetWidth, targetWidth * LOGO_ASPECT); // Starts at exactly this size (matching the baked wordmark), grows, then // returns to it - never smaller, never settling at the grown size. const baseScale = logo.scale; this.tweens.add({ targets: logo, scale: baseScale * 1.08, duration: 1400, yoyo: true, repeat: -1, ease: 'Sine.easeInOut', }); } // Phaser's Text only supports one stroke pass, so a black-then-white // double outline (readable against literally any part of the busy poster // art behind it) is built from two stacked Text objects at the same // position: a white one (its stroke forms the outer ring) behind a // slightly less-stroked black one (whose own stroke/fill covers the // inner ring) - the exposed sliver of the white layer around the black // one's edge is what reads as the white outline. _buildPrompt() { const x = GAME_WIDTH * PROMPT_X_FRAC; const y = GAME_HEIGHT * PROMPT_Y_FRAC; const text = 'Press any key or tap to continue'; // A thick outer stroke relative to font size was filling in letterform // counters (the holes in a/e/o) on a bold monospace font, which read as // "blobby" rather than crisp - thinner strokes on a slightly bigger font // keep the outline readable without eating the letter shapes themselves. const fontSize = `${20 * WORLD_SCALE}px`; const outline = this.add.text(x, y, text, { fontFamily: 'monospace', fontSize, fontStyle: 'bold', color: '#ffffff', stroke: '#ffffff', strokeThickness: 5 * WORLD_SCALE, }).setOrigin(0.5); const fill = this.add.text(x, y, text, { fontFamily: 'monospace', fontSize, fontStyle: 'bold', color: '#000000', stroke: '#000000', strokeThickness: 2 * WORLD_SCALE, }).setOrigin(0.5); // Shallow range (was dipping to 0.3) - low enough to still read as a // pulse, never low enough to actually wash the double-stroke out against // the bright sky behind it. this.tweens.add({ targets: [outline, fill], alpha: { from: 1, to: 0.65 }, duration: 700, yoyo: true, repeat: -1, }); } }