monsterplex/src/scenes/IntroScene.js

124 lines
4.9 KiB
JavaScript

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,
});
}
}