feat: replace text titles with poster art backgrounds and animated logo
- IntroScene: use bg_main.png poster art as full background, layer logo.png with a breathing tween aligned to the baked wordmark via fitted similarity transform constants, and add a double-stroke "Press any key" prompt (replaced auto-advance with input-only trigger). - MainMenuScene: replace text title with bg_menu.png background and centered logo.png overlay; add semi-transparent background pill to instructions text for readability over the treeline. - LevelSelectScene: add bg_menu.png background and background pill to back button text for contrast. - assetManifest: register bg_main, logo, and bg_menu textures. - New assets: bg_menu.png, ctc.png, paid.png, bg_main.png, logo.png.
This commit is contained in:
parent
e7cb1d907a
commit
6cf184fd4d
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 876 KiB |
|
|
@ -2,6 +2,35 @@ 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');
|
||||
|
|
@ -11,36 +40,84 @@ export default class IntroScene extends Phaser.Scene {
|
|||
this.cameras.main.setBackgroundColor('#8fc7e8');
|
||||
playMenuMusic(this);
|
||||
|
||||
this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 60 * WORLD_SCALE, 'MONSTERPLEX', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${52 * WORLD_SCALE}px`,
|
||||
color: '#1a1f29',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add.image(0, 0, 'bg_main').setOrigin(0, 0).setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2, 'a school bus survival ride', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${18 * WORLD_SCALE}px`,
|
||||
color: '#2255aa',
|
||||
}).setOrigin(0.5);
|
||||
this._buildLogo();
|
||||
this._buildPrompt();
|
||||
|
||||
const prompt = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 90 * WORLD_SCALE, 'Press any key or tap to continue', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${16 * WORLD_SCALE}px`,
|
||||
color: '#1a1f29',
|
||||
}).setOrigin(0.5);
|
||||
// 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: prompt,
|
||||
alpha: { from: 1, to: 0.2 },
|
||||
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,
|
||||
});
|
||||
|
||||
this._advance = () => this.scene.start('MainMenu');
|
||||
this.input.keyboard.once('keydown', this._advance);
|
||||
this.input.once('pointerdown', this._advance);
|
||||
this.time.delayedCall(6000, this._advance);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export default class LevelSelectScene extends Phaser.Scene {
|
|||
this.cameras.main.setBackgroundColor('#8fc7e8');
|
||||
playMenuMusic(this);
|
||||
|
||||
this.add.image(0, 0, 'bg_menu').setOrigin(0, 0).setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
this.add.text(GAME_WIDTH / 2, 60 * WORLD_SCALE, 'SELECT LEVEL', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${32 * WORLD_SCALE}px`,
|
||||
|
|
@ -32,10 +34,13 @@ export default class LevelSelectScene extends Phaser.Scene {
|
|||
this._createCard(x, y, cardWidth, cardHeight, level);
|
||||
});
|
||||
|
||||
// backgroundColor pill (same convention as PlayScene's HUD text) - this
|
||||
// corner sits right over bg_menu.png's dark treeline.
|
||||
const back = this.add.text(40 * WORLD_SCALE, GAME_HEIGHT - 40 * WORLD_SCALE, '< Menu', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${16 * WORLD_SCALE}px`,
|
||||
color: '#1a1f29',
|
||||
backgroundColor: '#ffffffaa',
|
||||
}).setInteractive({ useHandCursor: true });
|
||||
back.on('pointerdown', () => this.scene.start('MainMenu'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,29 @@ export default class MainMenuScene extends Phaser.Scene {
|
|||
this.cameras.main.setBackgroundColor('#8fc7e8');
|
||||
playMenuMusic(this);
|
||||
|
||||
this.add.text(GAME_WIDTH / 2, 90 * WORLD_SCALE, 'MONSTERPLEX', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${40 * WORLD_SCALE}px`,
|
||||
color: '#1a1f29',
|
||||
fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
this.add.image(0, 0, 'bg_menu').setOrigin(0, 0).setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
// logo.png's own texture has a lot of transparent padding around the
|
||||
// actual wordmark (see IntroScene's LOGO_* constants), so its display
|
||||
// width needs to be noticeably bigger than the visible logo is meant to
|
||||
// read as.
|
||||
const logoWidth = 620 * WORLD_SCALE;
|
||||
const logoAspect = 752 / 1392; // logo.png's native height/width
|
||||
this.add.image(GAME_WIDTH / 2, 90 * WORLD_SCALE, 'logo').setOrigin(0.5).setDisplaySize(logoWidth, logoWidth * logoAspect);
|
||||
|
||||
createButton(this, GAME_WIDTH / 2, GAME_HEIGHT / 2 - 20 * WORLD_SCALE, 'PLAY', () => {
|
||||
this.scene.start('LevelSelect');
|
||||
});
|
||||
|
||||
// backgroundColor pill (same convention as PlayScene's HUD text) - the
|
||||
// treeline in bg_menu.png sits right behind this text and would
|
||||
// otherwise swallow the dark-on-dark-green portion of it.
|
||||
this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 + 60 * WORLD_SCALE, 'Arrows/WASD to drive and lean.\nDon\'t let the g-force throw the kids out!', {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${14 * WORLD_SCALE}px`,
|
||||
color: '#1a1f29',
|
||||
align: 'center',
|
||||
backgroundColor: '#ffffffaa',
|
||||
}).setOrigin(0.5);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,12 @@ export const ASSET_MANIFEST = [
|
|||
{ key: 'bg_far', path: 'assets/backgrounds/bg_far.png', width: 256 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8, tileable: true },
|
||||
{ key: 'bg_mid', path: 'assets/backgrounds/bg_mid.png', width: 256 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x6fae7d, tileable: true },
|
||||
{ key: 'bg_near', path: 'assets/backgrounds/bg_near.png', width: 256 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x4c8a5c, tileable: true },
|
||||
// Intro screen's poster art (960x540 native design aspect) and the
|
||||
// standalone wordmark graphic IntroScene layers on top of it, animated -
|
||||
// see IntroScene._buildLogo.
|
||||
{ key: 'bg_main', path: 'assets/backgrounds/bg_main.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x2255aa },
|
||||
{ key: 'logo', path: 'assets/backgrounds/logo.png', width: 320 * WORLD_SCALE, height: 173 * WORLD_SCALE, kind: 'rect', color: 0xe040a0 },
|
||||
// Shared full-bleed background for MainMenuScene and LevelSelectScene.
|
||||
{ key: 'bg_menu', path: 'assets/backgrounds/bg_menu.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8 },
|
||||
{ key: 'favicon', path: 'assets/favicon.png', width: 32, height: 32, kind: 'rect', color: 0x2255aa },
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue