feat: Implement multi-layer parallax backgrounds, font loading, and improved game flow

- Added multiple background layers (bg-layer2, bg-layer3) with parallax scrolling effect
- Implemented proper background positioning and continuous scrolling logic
- Integrated custom 'Space' font for UI elements and game over text
- Updated menu scene integration and game flow with fade-out transition to menu
- Enhanced visual assets including logo display and improved background layer management
This commit is contained in:
Brian Fertig 2025-08-06 14:19:10 -06:00
parent 1af359e09f
commit 7b56d5b46a
22 changed files with 355 additions and 43 deletions

BIN
assets/ClassCoder.ttf Normal file

Binary file not shown.

BIN
assets/Vector.ttf Normal file

Binary file not shown.

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
assets/menu-alien.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
assets/menu-hunter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
assets/menu-ship.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
assets/space-age.otf Normal file

Binary file not shown.

View File

@ -30,12 +30,6 @@
<script src="phaser.min.js"></script> <script src="phaser.min.js"></script>
<!-- Import game modules using ES6 modules --> <!-- Import game modules using ES6 modules -->
<script type="module"> <script type="module" src="./src/config.js"></script>
import { config } from './src/config.js';
import { GameScene } from './src/scenes/GameScene.js';
const game = new Phaser.Game(config);
game.scene.add('GameScene', GameScene, true);
</script>
</body> </body>
</html> </html>

BIN
raw/class-coder.zip Normal file

Binary file not shown.

BIN
raw/saucer.zip Normal file

Binary file not shown.

BIN
raw/space-age.zip Normal file

Binary file not shown.

BIN
raw/vector.zip Normal file

Binary file not shown.

View File

@ -1,3 +1,6 @@
import { MenuScene } from './scenes/MenuScene.js';
import { GameScene } from './scenes/GameScene.js';
export const config = { export const config = {
type: Phaser.AUTO, type: Phaser.AUTO,
width: 600, width: 600,
@ -10,13 +13,14 @@ export const config = {
debug: false debug: false
} }
}, },
scene: { scene: [
preload: function() {}, MenuScene,
create: function() {}, GameScene,
update: function() {} ],
},
scale: { scale: {
mode: Phaser.Scale.FIT, mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH autoCenter: Phaser.Scale.CENTER_BOTH
} }
}; };
const game = new Phaser.Game(config);

View File

@ -33,8 +33,9 @@ export class GameScene extends Phaser.Scene {
// Load background images // Load background images
this.load.image('bg-layer1', 'assets/neon-city-bg.png'); this.load.image('bg-layer1', 'assets/neon-city-bg.png');
//this.load.image('bg-layer2', 'assets/bg-layer2.png'); this.load.image('bg-layer2', 'assets/neon-city-dark-clouds2.png');
//this.load.image('bg-layer3', 'assets/bg-layer3.png'); this.load.image('bg-layer3', 'assets/neon-city-dark-clouds.png');
this.load.image('logo', 'assets/logo.png');
// Load Sprites // Load Sprites
this.load.spritesheet('player-sprite', 'assets/player-sprite.png', { this.load.spritesheet('player-sprite', 'assets/player-sprite.png', {
@ -54,6 +55,9 @@ export class GameScene extends Phaser.Scene {
this.load.audio('player-death', 'assets/sounds/player-death.mp3'); this.load.audio('player-death', 'assets/sounds/player-death.mp3');
this.load.audio('enemy-shoot', 'assets/sounds/enemy-shoot.mp3'); this.load.audio('enemy-shoot', 'assets/sounds/enemy-shoot.mp3');
this.load.audio('next-wave', 'assets/sounds/next-wave.mp3'); this.load.audio('next-wave', 'assets/sounds/next-wave.mp3');
// Load Fonts
this.load.font('Space', 'assets/space-age.otf');
// Create simple placeholder graphics for now // Create simple placeholder graphics for now
this.createPlaceholderGraphics(); this.createPlaceholderGraphics();
@ -102,13 +106,13 @@ export class GameScene extends Phaser.Scene {
createBackgrounds() { createBackgrounds() {
// Create multiple background layers with different speeds // Create multiple background layers with different speeds
this.bgLayer1 = this.add.image(280, 30, 'bg-layer1').setOrigin(0.5); this.bgLayer1 = this.add.image(280, 30, 'bg-layer1').setOrigin(0.5);
//this.bgLayer2 = this.add.image(400, 300, 'bg-layer2').setOrigin(0.5); this.bgLayer2 = this.add.image(280, 30, 'bg-layer2').setOrigin(0.5);
//this.bgLayer3 = this.add.image(400, 300, 'bg-layer3').setOrigin(0.5); this.bgLayer3 = this.add.image(280, 30, 'bg-layer3').setOrigin(0.5);
// Set different speeds for parallax effect (smaller = slower) // Set different speeds for parallax effect (smaller = slower)
this.bgSpeed1 = 0.2; this.bgSpeed1 = 0.2;
//this.bgSpeed2 = 0.5; this.bgSpeed2 = 0.5;
//this.bgSpeed3 = 0.8; this.bgSpeed3 = 0.8;
} }
update(time, delta) { update(time, delta) {
@ -141,19 +145,19 @@ export class GameScene extends Phaser.Scene {
updateBackgrounds() { updateBackgrounds() {
// Move background layers at different speeds // Move background layers at different speeds
this.bgLayer1.y += this.bgSpeed1; this.bgLayer1.y += this.bgSpeed1;
//this.bgLayer2.x -= this.bgSpeed2; this.bgLayer2.y += this.bgSpeed2;
//this.bgLayer3.x -= this.bgSpeed3; this.bgLayer3.y += this.bgSpeed3;
// Reset positions to create continuous scrolling effect // Reset positions to create continuous scrolling effect
if (this.bgLayer1.y > 736) { if (this.bgLayer1.y > 736) {
this.bgLayer1.y = 30; // Width of game + layer width this.bgLayer1.y = 0;
}
if (this.bgLayer2.y > 1536) {
this.bgLayer2.y = 0;
}
if (this.bgLayer3.y > 1536) {
this.bgLayer3.y = 0;
} }
// if (this.bgLayer2.x < -400) {
// this.bgLayer2.x = 1200;
// }
// if (this.bgLayer3.x < -400) {
// this.bgLayer3.x = 1200;
// }
} }
setupMouseInput() { setupMouseInput() {
@ -229,17 +233,18 @@ export class GameScene extends Phaser.Scene {
this.scoreText = this.add.text(16, 16, 'Score: 0', { this.scoreText = this.add.text(16, 16, 'Score: 0', {
fontSize: '28px', fontSize: '28px',
fill: '#ffffff', fill: '#ffffff',
fontFamily: 'Arial' fontFamily: 'Space, Arial'
}); });
// Create game title // Create game title
this.titleText = this.add.text(300, 50, 'Zenith Vector', { // this.titleText = this.add.text(300, 50, 'Zenith Vector', {
fontSize: '40px', // fontSize: '40px',
fill: '#00ffcc', // fill: '#00ffcc',
fontFamily: 'Arial', // fontFamily: 'Arial',
align: 'center' // align: 'center'
}); // });
this.titleText.setOrigin(0.5); // this.titleText.setOrigin(0.5);
this.titleText = this.add.image(300, 50, 'logo').setScale(.1).setOrigin(0.5);
} }
shoot(pointer) { shoot(pointer) {
@ -365,14 +370,24 @@ export class GameScene extends Phaser.Scene {
const gameOverText = this.add.text(300, 400, 'GAME OVER', { const gameOverText = this.add.text(300, 400, 'GAME OVER', {
fontSize: '54px', fontSize: '54px',
fill: '#ff3366', fill: '#ff3366',
fontFamily: 'Arial' fontFamily: 'Space, Arial'
}); });
gameOverText.setOrigin(0.5); gameOverText.setOrigin(0.5);
// Restart game after delay // Return to menu after delay
// this.time.delayedCall(2000, () => { this.time.delayedCall(5000, () => {
// this.scene.restart(); // Fade camera out before returning to menu
// }); this.tweens.add({
targets: this.cameras.main,
alpha: 0, // Fade to transparent
duration: 1000, // 1 second fade
ease: 'Linear',
onComplete: () => {
this.bgMusic.stop();
this.scene.start('MenuScene');
}
});
});
} }
startWave(waveNumber) { startWave(waveNumber) {
@ -381,9 +396,9 @@ export class GameScene extends Phaser.Scene {
// Add wave indicator text // Add wave indicator text
const waveText = this.add.text(300, 150, `WAVE ${waveNumber + 1}`, { const waveText = this.add.text(300, 150, `WAVE ${waveNumber + 1}`, {
fontSize: '48px', fontSize: '48px',
fill: '#ffff00', fill: '#ffff00',
fontFamily: 'Arial' fontFamily: 'Space, Arial'
}); });
waveText.setOrigin(0.5); waveText.setOrigin(0.5);

299
src/scenes/MenuScene.js Normal file
View File

@ -0,0 +1,299 @@
export class MenuScene extends Phaser.Scene {
constructor() {
super('MenuScene');
}
preload() {
this.load.image('menu-logo','assets/logo.png');
this.load.image('menu-ship','assets/menu-ship.png');
this.load.image('menu-hunter','assets/menu-hunter.png');
this.load.image('menu-alien','assets/menu-alien.png');
this.load.image('reticle', 'assets/reticle.png');
this.load.audio('menu-bgMusic', 'assets/music/NeonVelocity.mp3');
// Load custom font
this.load.font('Space', 'assets/space-age.otf');
this.load.font('coder', 'assets/ClassCoder.ttf');
}
create() {
// Create menu background and title
this.add.rectangle(300, 400, 600, 800, 0x111111);
// Create starfield effect
this.createStarfield();
const logo = this.add.image(300, 250, 'menu-logo').setOrigin(0.5).setScale(0.5).setAlpha(0.7);
const menuHunter = this.add.image(450, 700, 'menu-hunter').setScale(0.6).setOrigin(0.5).setAlpha(0);
const menuShip = this.add.image(900, -300, 'menu-ship').setOrigin(0.5).setScale(.6);
const menuAlien = this.add.image(150, 700, 'menu-alien').setScale(0.6).setOrigin(0.5).setAlpha(0);
this.tweens.add({
targets: menuShip,
x: 500,
y: 100,
duration: 2000,
ease: 'Cubic',
delay: 3000
});
this.tweens.add({
targets: logo,
alpha: 1,
scale: 1,
duration: 5000,
ease: 'Bounce',
});
this.tweens.add({
targets: [menuHunter, menuAlien],
alpha: 1,
duration: 3000,
delay: 4000,
ease: 'Bounce'
})
// Controls display with typing animation
const controlsText = 'Controls: A and D to go Left and Right. Mouse to Aim and Fire';
this.controlsText = this.add.text(300, 500, '', {
fontFamily: 'Coder, Arial',
fontSize: '16px',
fill: '#00ff00'
}).setOrigin(0.5);
// Typing animation
this.time.delayedCall(1000, () => {
let i = 0;
const typingTimer = this.time.addEvent({
delay: 50, // Characters per second
callback: () => {
if (i < controlsText.length) {
this.controlsText.text += controlsText.charAt(i);
i++;
} else {
typingTimer.remove();
}
},
callbackScope: this,
loop: true
});
});
// Menu button
this.startButton = this.add.text(300, 550, ' START GAME ', {
fontFamily: 'Space, Arial',
fontSize: '48px',
fill: '#fff'
}).setOrigin(0.5);
// Add color cycling effect for synthwave colors
const synthwaveColors = ['#ffffff', '#ff00ff', '#00ffff', '#ffff00', '#ff00cc'];
let colorIndex = 0;
this.time.addEvent({
delay: 1500,
callback: () => {
colorIndex = (colorIndex + 1) % synthwaveColors.length;
this.startButton.setFill(synthwaveColors[colorIndex]);
},
loop: true
});
this.tweens.add({
targets: this.startButton,
scale: 1.2,
duration: 1500,
repeat: -1,
ease: 'Sine',
yoyo: true,
});
this.startButton.setInteractive();
this.startButton.on('pointerdown', () => {
// Start the actual game scene
this.bgMusic.stop();
this.scene.start('GameScene');
});
// Play background music
this.bgMusic = this.sound.add('menu-bgMusic', { loop: true });
this.bgMusic.play();
// Setup mouse input for shooting and reticle
this.setupMouseInput();
// Add scanline effect (narrow and moving)
this.scanlines = this.add.graphics({ x: 0, y: 0 });
this.scanlines.fillStyle(0x00ff00, 0.15); // Green scanlines with moderate opacity
// Create narrow horizontal lines that move vertically
for (let i = 0; i < 20; i++) {
const y = (i * 40) % 800;
this.scanlines.fillRect(0, y, 600, 2); // Narrow lines (2px height)
}
// Add VHS artifacts
this.vhsArtifacts = this.add.graphics({ x: 0, y: 0 });
this.vhsArtifacts.fillStyle(0xff00ff, 0.1); // Magenta artifacts
this.vhsArtifacts.fillRect(0, 0, 600, 800);
// Add VHS artifacts 2
this.vhsArtifacts2 = this.add.graphics({ x: 0, y: 0 });
this.vhsArtifacts2.fillStyle(0xff00ff, 0.1); // Magenta artifacts
this.vhsArtifacts2.fillRect(0, 0, 600, 800);
this.tweens.add({
targets: this.vhsArtifacts2,
alpha: 0.1,
duration: 10000,
yoyo: true,
repeat: -1
});
// Start artifact effects
this.artifactTimer = this.time.addEvent({
delay: 200,
callback: this.updateVHSArtifacts,
callbackScope: this,
loop: true
});
}
createStarfield() {
// Create a graphics object for the starfield
this.starfield = this.add.graphics({ x: 0, y: 0 });
// Star configuration
this.stars = [];
const starCount = 200;
// Initialize stars with random positions and z-depths
for (let i = 0; i < starCount; i++) {
const star = {
x: Math.random() * 600,
y: Math.random() * 800,
z: Math.random() * 10 + 1, // Depth from 1 to 11
size: Math.random() * 2 + 1,
speed: Math.random() * 0.5 + 0.2 // Significantly reduced speed
};
// Set color based on depth (closer stars are brighter)
const brightness = Math.max(0.5, 1 - star.z / 15); // Closer stars brighter
const grayValue = Math.floor(150 + brightness * 105);
star.color = (grayValue << 16) | (grayValue << 8) | grayValue;
this.stars.push(star);
}
// Change direction timer - increased to make it more subtle
this.directionChangeTimer = this.time.addEvent({
delay: 5000, // Increase delay to 5 seconds
callback: this.changeStarDirection,
callbackScope: this,
loop: true
});
}
update() {
// Update star positions
if (this.starfield && this.stars) {
this.starfield.clear();
for (let i = 0; i < this.stars.length; i++) {
const star = this.stars[i];
// Move stars toward camera (decrease z) - greatly reduced speed
star.z -= star.speed * 0.02;
// Reset stars that move too close to camera
if (star.z <= 0) {
star.z = Math.random() * 10 + 1; // Reset with new depth
star.x = Math.random() * 600; // Random x position
star.y = Math.random() * 800; // Random y position
// Recalculate color based on new depth
const brightness = Math.max(0.5, 1 - star.z / 15);
const grayValue = Math.floor(150 + brightness * 105);
star.color = (grayValue << 16) | (grayValue << 8) | grayValue;
}
// Apply perspective projection to create parallax effect
const scale = 20 / (star.z + 20); // Scale based on depth
const screenX = star.x * scale;
const screenY = star.y * scale;
// Draw the star with its color and scaled size
this.starfield.fillStyle(star.color);
this.starfield.fillCircle(screenX, screenY, star.size * scale);
}
}
// Update scanline position
if (this.scanlines) {
this.scanlines.y = (this.scanlines.y + 0.5) % 800;
}
}
updateVHSArtifacts() {
// Randomly update VHS artifacts
if (this.vhsArtifacts && Math.random() > 0.7) {
// Clear previous artifacts but keep some for flickering effect
this.vhsArtifacts.clear();
// Add random color shifts and distortions
for (let i = 0; i < 5; i++) {
const x = Math.random() * 600;
const y = Math.random() * 800;
const width = Math.random() * 10 + 1;
const height = Math.random() * 20 + 5;
// Random color (neon colors for synthwave)
const colors = [0xff00ff, 0x00ffff, 0xffff00];
const color = Phaser.Display.Color.GetColor32(
...colors[Math.floor(Math.random() * colors.length)].toString(16).split('').map(c => parseInt(c, 16) * 17)
);
// Use higher opacity for flickering effect (instead of very low opacity)
this.vhsArtifacts.fillStyle(color, Math.random() * 0.3 + 0.1); // Increased from 0.05 to 0.3-0.1 range
this.vhsArtifacts.fillRect(x, y, width, height);
}
}
}
changeStarDirection() {
// Change direction of all stars (they'll move toward camera)
for (let i = 0; i < this.stars.length; i++) {
const star = this.stars[i];
// Randomly adjust speed and depth - much slower changes
star.speed = Math.random() * 1 + 0.5; // Reduced speed range
star.z += (Math.random() * 0.5 - 0.25); // Very subtle z position change
// Keep stars within reasonable bounds
if (star.z < 0.5) {
star.z = 0.5;
} else if (star.z > 15) {
star.z = 15;
}
// Recalculate color based on new depth and speed
const brightness = Math.max(0.5, 1 - star.z / 15);
const grayValue = Math.floor(150 + brightness * 105);
star.color = (grayValue << 16) | (grayValue << 8) | grayValue;
}
}
setupMouseInput() {
// Create mouse reticle as sprite (replacing the circle)
this.reticle = this.add.sprite(0, 0, 'reticle');
this.reticle.setOrigin(0.5);
this.reticle.preFX.addGlow(0x00ffcc, .7);
// Update reticle position with mouse
this.input.on('pointermove', (pointer) => {
if (this.gameOver) return;
this.reticle.x = pointer.x;
this.reticle.y = pointer.y;
});
}
}