| Server IP : 167.235.67.158 / Your IP : 216.73.216.95 Web Server : Apache System : Linux ubuntu-8gb-nbg1-1 6.8.0-111-generic #111-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 11 23:16:02 UTC 2026 x86_64 User : upstairsbar.co.uk ( 982) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bc.the-word.com/openseal/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenSea Surf</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap" rel="stylesheet">
<!-- Howler.js for audio -->
<script src="https://cdn.jsdelivr.net/npm/howler@2.2.4/dist/howler.min.js"></script>
<!-- FarcadeSDK for game integration -->
<script src="https://cdn.jsdelivr.net/npm/@farcade/game-sdk@latest/dist/index.min.js"></script>
<!-- Phaser and PhaserBox2D will be imported as ES modules in the script below -->
<style>
html,
body {
height: 100%;
overflow: hidden;
position: fixed;
width: 100%;
color: #fff;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #000;
font-family: 'Fredoka', sans-serif;
font-weight: 400;
touch-action: none;
}
canvas {
position: relative;
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
background: #000;
overflow: hidden;
aspect-ratio: 5 / 8;
width: min(100vw, calc(100vh * 5/8));
height: min(100vh, calc(100vw * 8/5));
}
#startScreen,
#howToPlay {
position: absolute;
top: 0;
left: 50%;
width: min(100vw, calc(100vh * 5/8));
height: min(100vh, calc(100vw * 8/5));
z-index: 10;
transform: translateX(-50%);
transition: opacity 0.5s ease-out, visibility 0.5s ease-out;
}
#howToPlay {
display: none;
}
#startScreen.fade-out {
opacity: 0;
visibility: hidden;
}
#logo {
margin-top: 50px;
width: 349px;
height: 234px;
background: url('logo.png') no-repeat center center;
background-size: contain;
opacity: 0;
transform: translateY(-40px) scale(1.05);
animation:
logoDrop 0.65s 0.1s cubic-bezier(.27, 1.4, .48, 1) forwards,
logoBob 2.4s 0.9s ease-in-out infinite;
}
@keyframes logoDrop {
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes logoBob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(3px);
}
}
.screen-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
#btns {
display: flex;
flex-direction: column;
align-items: center;
margin: auto 0 100px;
transform: scale(0);
opacity: 0;
animation:
btnEnter 0.5s 0.2s cubic-bezier(.25, 1.3, .38, 1) forwards
}
@keyframes btnEnter {
to {
transform: scale(1);
opacity: 1;
}
}
.btn {
background: #fff;
border: 0;
padding: 0 10px;
border-radius: 45px;
height: 80px;
margin-top: 20px;
cursor: pointer;
transition: transform .1s ease-in-out;
}
.btn:hover {
transform: scale(1.05);
}
.btn span {
border: 5px solid #003c80;
display: flex;
justify-content: center;
align-items: center;
font-size: 32px;
font-family: 'Fredoka', sans-serif;
font-weight: 600;
color: #003c80;
border-radius: 45px;
padding: 5px 40px;
}
.btn.small {
height: 60px;
opacity: .8;
}
.btn.small span {
font-size: 20px;
padding: 5px 25px;
}
#howToPlay {
background: #003c80;
padding: 30px;
height: 100%;
box-sizing: border-box;
}
#howToPlay h1 {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
font-size: 28px;
}
#howToPlay .screens {
position: relative;
height: 100%;
width: 100%;
margin-bottom: 20px;
}
#howToPlay .screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
#howToPlay .screen {
/* EVERY card */
transition: transform .48s cubic-bezier(.45, 1.15, .35, 1),
opacity .32s ease-out;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* card under the top one → just a quiet offset */
#howToPlay .screen.stack {
transform: translateY(22px) scale(.94);
}
/* the active card you can read / click */
#howToPlay .screen.active {
transform: translateY(0) scale(1);
pointer-events: auto;
}
/* 1‑B When it’s swiped away (Next) --------------------------------- */
#howToPlay .screen.removed-right {
transform: translateX(110%) rotate(6deg) scale(.90);
opacity: 0;
}
#howToPlay .screen img {
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
border: 10px solid #fff;
border-radius: 10px;
box-sizing: border-box;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 30px;
}
.circleBtn {
position: absolute;
top: 50%;
left: -20px;
transform: translateY(-50%);
width: 70px;
height: 70px;
border-radius: 50%;
background: #fff;
z-index: 11;
box-shadow: -5px 5px 0px 0px rgba(0, 60, 128, .9);
-webkit-box-shadow: -5px 5px 0px 0px rgba(0, 60, 128, .9);
-moz-box-shadow: -5px 5px 0px 0px rgba(0, 60, 128, .9);
transition: all .25s ease-in-out;
cursor: pointer;
}
.circleBtn:hover,
.circleBtn:focus,
.circleBtn:active {
transform: translateY(-50%) scale(1.07);
}
.circleBtn span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: url(arrow.jpg) no-repeat center center;
background-size: contain;
width: 20px;
height: 23px;
}
.nextBtn {
left: auto;
right: -20px;
}
.prevBtn span {
transform: translate(-50%, -50%) rotate(180deg);
}
.close {
cursor: pointer;
}
</style>
</head>
<body>
<div id="startScreen">
<div class="screen-content">
<div id="logo"></div>
<div id="btns">
<button class="btn" onclick="startGame()"><span>Set Sail</span></button>
<button class="btn small" onclick="howToPlay()"><span>How to Play</span></button>
</div>
</div>
</div>
<div id="howToPlay">
<div class="screen-content">
<header>
<span class="close" onclick="closeHowToPlay()">Exit</span>
<h1>How to Play</h1>
<span class="viewCount">1/5</span>
</header>
<div class="prevBtn circleBtn" onclick="prevScreen()"><span></span></div>
<div class="nextBtn circleBtn" onclick="nextScreen()"><span></span></div>
<div class="screens">
<div class="screen">
<img src="howto-5.jpg">
</div>
<div class="screen">
<img src="howto-4.jpg">
</div>
<div class="screen">
<img src="howto-3.jpg">
</div>
<div class="screen">
<img src="howto-2.jpg">
</div>
<div class="screen active">
<img src="howto-1.jpg">
</div>
</div>
</div>
</div>
<script type="module">
// Import Phaser and PhaserBox2D as ES modules
import * as Phaser from 'https://cdn.jsdelivr.net/npm/phaser@3.90.0/dist/phaser.esm.min.js';
import {
AddSpriteToWorld, RemoveSpriteFromWorld, SetWorldScale, GetWorldScale, SpriteToBox, SpriteToCircle,
UpdateWorldSprites, b2DestroyBody, CreateWorld, STATIC, WorldStep, b2World_Draw,
b2DefaultWorldDef, b2Vec2, b2MulSV, b2Body_ApplyForceToCenter,
b2Body_GetLinearVelocity, b2Body_SetLinearVelocity
} from 'https://cdn.jsdelivr.net/npm/phaser-box2d@1.1.0/dist/PhaserBox2D.min.js';
// Configuration settings for the stacking game.
const gameConfig = {
gravityY: -10,
cameraTransitionTime: 200,
terrainHeight: 0.5,
terrainAmplitudeMin: 5,
terrainAmplitudeMax: 10,
terrainAmplitudeSize: 10,
terrainSegmentsMin: 20,
terrainSegmentsMax: 30,
terrainSegmentLength: 10,
terrainToGenerate: 10,
// Terrain visual configuration
terrainLineColor: 0x003c80, // Top terrain line color
terrainLineThickness: 8, // Top terrain line thickness
terrainLayer2Color: 0x0378d7, // Second layer color
terrainLayer2Thickness: 50, // Second layer thickness
terrainLayer3Color: 0x0062b4, // Bottom fill color
// Sky gradient configuration
skyColorBottom: 0x2db5e6, // Sky color at bottom
skyColorTop: 0x158de5, // Sky color at top
// Legacy settings (keeping for compatibility)
terrainColor: 0x3d6728,
terrainPointsColor: 0xFF6728,
terrainStartX: 250,
removeSegmentX: 500,
birdDiveForce: 15,
birdDiveDirection: new b2Vec2(0, -10),
birdFireForce: 200,
birdFireDirection: new b2Vec2(5, 10),
// Minimum velocity to prevent rolling backward (like original TinySeal)
minVelocityX: 5,
minVelocityY: -40,
// Coin system configuration
coinsPerGroup: { min: 3, max: 5 },
coinSpawnChance: 0.1, // 10% chance to spawn coins per terrain segment (adjustable)
coinRadius: 12,
coinColor: 0xFF8C00, // Orange color
coinGap: 5, // Gap between coins in pixels
coinHeightOffset: 40, // How far above terrain to place coins
coinCollectionDistance: 30, // Distance for collecting coins
coinPoints: 10, // Points per coin collected
// Power-up system configuration
powerUpSpawnChance: 0.025, // 1% chance to spawn power-up (adjustable)
powerUpRadius: 15,
powerUpColor: 0xffed4e, // Deep sky blue
powerUpGlowColor: 0xffed4e, // Sky blue glow
powerUpHeightOffset: 25, // How far above terrain to place power-up
powerUpCollectionDistance: 35, // Distance for collecting power-up
powerUpDuration: 2000, // 2 seconds in milliseconds (adjustable)
powerUpVelocityBoost: 30, // Max velocity when powered up
// Spawn delay configuration
noSpawnSegments: 3, // Don't spawn items in first X terrain segments (adjustable)
// Parallax background configuration
cloudSpawnChance: 0.3, // 30% chance per terrain segment
cloudParallaxSpeed: 0.1, // Clouds move at 10% of camera speed (far away)
cloudMinScale: 0.75,
cloudMaxScale: 1.25,
cloudTopAreaHeight: 100, // Top 25% of 800px screen
mountainSpawnChance: 0.3, // 40% chance per terrain segment
mountainParallaxSpeed: 0.3, // Mountains move at 30% of camera speed (closer)
mountainMinScale: 0.75,
mountainMaxScale: 2,
mountainBottomY: 500, // Bottom of mountains (below lowest possible wave at 500px)
// Wave chasing system configuration
waveSpeed: 3, // How fast the wave moves forward (pixels per frame)
waveStartDelay: 4000, // Delay before wave starts moving (milliseconds)
waveInitialDistance: 1200, // How far behind the ship the wave starts
waveMaxDistance: 1200, // Maximum distance the wave can fall behind
waveMinDistance: 250, // Minimum distance before game over (wave right edge reaches ship)
// Progressive difficulty configuration
difficulty: {
// Wave speed progression
waveSpeedMin: 2, // Starting wave speed (easier than current 3)
waveSpeedMax: 5, // Maximum wave speed cap
waveSpeedIncreaseRate: 0.00001, // How fast wave speed increases per distance unit
// Terrain difficulty progression
terrainAmplitudeStart: 5, // Starting terrain amplitude (more interesting than 3, but not too flat)
terrainAmplitudeEndMin: 7, // Ending minimum amplitude (more gradual increase)
terrainAmplitudeEndMax: 12, // Ending maximum amplitude (less dramatic than 15)
terrainComplexityRate: 0.0002, // How fast terrain gets more complex
// Distance milestones for difficulty scaling
maxDifficultyDistance: 8000, // Distance at which maximum difficulty is reached (longer progression)
// Additional difficulty factors
powerupScarcityRate: 0.0001, // How much rarer powerups become over time
coinSpacingRate: 0.0001 // How much more spread out coins become
}
}
// State variables to track game progress.
const gameProperties = {
currentHeight: 0,
currentStack: -1,
maxHeightAchieved: 0,
terrainSegments: [],
terrainPoints: [], // Track all terrain curve points for smooth rendering
segmentGroup: null,
terrainLayer2Graphics: null, // Graphics object for second layer
terrainLayer3Graphics: null, // Graphics object for bottom fill
gameStarted: false,
isDiving: false,
distanceToNextTerrain: 0,
distanceTravelled: 0,
birdStartX: 0,
score: 0,
coinScore: 0, // Separate tracking for coin points
lastPoint: null,
terrainSegmentCount: 0, // Track number of terrain segments generated
// Coin system
coinPool: [],
activeCoinGroups: [],
coinGroup: null,
// Power-up system
powerUpPool: [],
activePowerUps: [],
powerUpGroup: null,
isPoweredUp: false,
powerUpEndTime: 0,
particleEmitter: null,
particleEmitterGlow: null,
// Ship rotation tracking (like original TinySeal)
prevVelocities: [],
maxPrevVelocities: 5,
nextVelIndex: 0,
// Parallax background system
cloudsForPool: 50,
mountainsForPool: 50,
cloudPool: [],
mountainPool: [],
activeClouds: [],
activeMountains: [],
cloudGroup: null,
mountainGroup: null,
// Wave chasing system
wavePosition: 0, // Current wave X position
waveStartTime: 0, // When the wave started chasing
waveActive: false, // Whether the wave is actively chasing
gamePaused: false, // Whether game is paused due to wave catching ship
shipCaught: false, // Whether ship has been caught by wave (freeze physics)
shipFinalX: 0, // Ship's final X position when caught
shipFinalY: 0, // Ship's final Y position when caught
shipFinalRotation: 0, // Ship's final rotation when caught
}
/**
* Creates a seamless grain texture & stores it in Phaser’s texture manager.
*
* @param {string} key – texture key
* @param {number} baseColor – 0xRRGGBB
* @param {number} intensity – 0‑1 (Illustrator’s “Intensity” ÷ 100)
* @param {number} contrast – 0‑1 (Illustrator’s “Contrast” ÷ 100)
* @param {number} size – texture side in px (should be power‑of‑two if you plan to repeat)
*/
function buildGrainTexture(scene, key, baseColor, intensity = 0.15, contrast = 0.44, size = 128) {
const tex = scene.textures.createCanvas(key, size, size);
const ctx = tex.getContext();
// --- 1. fill base colour ---
ctx.fillStyle = Phaser.Display.Color.IntegerToColor(baseColor).rgba;
ctx.fillRect(0, 0, size, size);
// --- 2. add per‑pixel noise ---
const img = ctx.getImageData(0, 0, size, size);
const data = img.data;
// contrast formula: https://en.wikipedia.org/wiki/Contrast_(vision)#Image_contrast
const c = Phaser.Math.Clamp(contrast, 0, 1);
const factor = (259 * (c * 255 + 255)) / (255 * (259 - c * 255));
for (let p = 0; p < data.length; p += 4) {
const n = (Math.random() * 2 - 1) * intensity * 128;
for (let ch = 0; ch < 3; ch++) {
let v = data[p + ch] + n; // add noise
// --- no contrast expansion ---
data[p + ch] = Phaser.Math.Clamp(v, 0, 255);
}
}
ctx.putImageData(img, 0, 0);
// --- 3. very slight blur for “Soft” grain ---
ctx.filter = 'blur(0.35px)';
ctx.drawImage(tex.getSourceImage(), 0, 0);
ctx.filter = 'none'; // reset
tex.refresh();
}
class Example extends Phaser.Scene {
constructor() {
super({ key: 'Game' });
}
preload() {
//Load font
this.load.font('Fredoka', 'Fredoka-Regular.ttf', 'truetype');
this.load.font('Fredoka-SemiBold', 'Fredoka-SemiBold.ttf', 'truetype');
// Load coin and power-up images
this.load.image('coin', 'coin.png');
this.load.image('lightning', 'lightning.png');
// Load parallax background images
this.load.image('cloud', 'cloud.png');
this.load.image('mountain', 'mountain.png');
// Load wave image for the chasing mechanic
this.load.image('wave', 'wave.png');
this.load.image('landing', 'landing.png');
// Load progress bar fill image (86x56 @ 2x, so 43x28 @ 1x)
this.load.image('progress', 'progress.png');
// Load ship sprite (now with 3 sprites side by side)
this.load.spritesheet('ship', 'ship.png', {
frameWidth: 186, // Keep same frame width as before for each individual sprite
frameHeight: 166 // Full height
});
}
create() {
// Send ready event to FarcadeSDK
if (window.FarcadeSDK) {
window.FarcadeSDK.singlePlayer.actions.ready();
console.log('Sent ready event to FarcadeSDK');
}
this.initVariables();
this.initializePhysicsWorld();
this.initializeGameElements();
this.initCamera();
this.addBlueBackground();
this.scene.launch('GameUi');
this.GameUi = this.scene.get('GameUi');
}
initVariables() {
gameProperties.terrainSegments = [];
gameProperties.terrainPoints = [];
gameProperties.segmentGroup = this.add.group();
gameProperties.coinGroup = this.add.group();
gameProperties.coinPool = [];
gameProperties.activeCoinGroups = [];
gameProperties.powerUpGroup = this.add.group();
gameProperties.powerUpPool = [];
gameProperties.activePowerUps = [];
gameProperties.terrainSegmentCount = 0;
// Initialize parallax background
gameProperties.cloudPool = [];
gameProperties.mountainPool = [];
gameProperties.activeClouds = [];
gameProperties.activeMountains = [];
gameProperties.cloudGroup = this.add.group();
gameProperties.mountainGroup = this.add.group();
// Initialize velocity tracking array
gameProperties.prevVelocities = [];
for (let i = 0; i < gameProperties.maxPrevVelocities; i++) {
gameProperties.prevVelocities.push({ x: 0, y: 0 });
}
gameProperties.nextVelIndex = 0;
// Initialize terrain graphics layers
gameProperties.terrainLayer3Graphics = this.add.graphics();
gameProperties.terrainLayer3Graphics.setDepth(1);
gameProperties.terrainLayer2Graphics = this.add.graphics();
gameProperties.terrainLayer2Graphics.setDepth(2);
// Initialize audio system
this.initializeAudio();
this.initializeCoinPool();
this.initializePowerUpPool();
this.initializeBackgroundPools();
this.initializeParticleSystem();
}
initializeAudio() {
// Audio system using Howler.js for better compatibility
// Ambience background music (loop at low volume)
gameProperties.ambienceSound = new Howl({
src: ['Ambience.mp3'],
volume: 0.15,
loop: true
});
// Sequential coin collection sounds (Coin1.mp3 through Coin6.mp3)
gameProperties.coinSounds = [];
for (let i = 1; i <= 6; i++) {
gameProperties.coinSounds.push(new Howl({
src: [`Coin${i}.mp3`],
volume: 0.7
}));
}
// Power-up/thruster sound
gameProperties.thrusterSound = new Howl({
src: ['Thruster.mp3'],
volume: 0.8
});
// Game over sound
gameProperties.gameOverSound = new Howl({
src: ['Gameover.mp3'],
volume: 0.9
});
// Down sound for pointer/touch events
gameProperties.downSound = new Howl({
src: ['Down.mp3'],
volume: 0.6
});
// Splash sound for landing after airtime
gameProperties.splashSound = new Howl({
src: ['Splash.mp3'],
volume: 0.2
});
// Coin collection sequence tracking
gameProperties.coinSequenceIndex = 0;
gameProperties.lastCoinCollectionTime = 0;
gameProperties.coinSequenceTimeout = 2000; // 2 seconds to reset sequence
// Audio state
gameProperties.ambienceStarted = false;
gameProperties.isMuted = false;
// Collision-based airtime tracking for splash sound
gameProperties.isInAir = false;
gameProperties.airStartTime = 0;
gameProperties.minAirtimeForSplash = 300; // 0.3 second minimum airtime
gameProperties.terrainContactCount = 0; // Track number of terrain contacts
console.log('Audio system initialized');
}
// Audio helper functions
startAmbienceIfNeeded() {
if (!gameProperties.isMuted && !gameProperties.ambienceStarted && gameProperties.ambienceSound) {
gameProperties.ambienceSound.play();
gameProperties.ambienceStarted = true;
console.log('Ambience music started');
}
}
playCoinCollectionSound() {
this.startAmbienceIfNeeded(); // Start ambience on first interaction
if (gameProperties.isMuted) return;
const currentTime = Date.now();
// Check if enough time has passed to reset the sequence
if (currentTime - gameProperties.lastCoinCollectionTime > gameProperties.coinSequenceTimeout) {
gameProperties.coinSequenceIndex = 0;
}
// Play the current coin sound in sequence
if (gameProperties.coinSounds[gameProperties.coinSequenceIndex]) {
gameProperties.coinSounds[gameProperties.coinSequenceIndex].play();
console.log(`Playing coin sound ${gameProperties.coinSequenceIndex + 1}`);
}
// Update sequence tracking
gameProperties.coinSequenceIndex = (gameProperties.coinSequenceIndex + 1) % 6;
gameProperties.lastCoinCollectionTime = currentTime;
}
playThrusterSound() {
this.startAmbienceIfNeeded(); // Start ambience on first interaction
if (!gameProperties.isMuted && gameProperties.thrusterSound) {
gameProperties.thrusterSound.play();
console.log('Playing thruster sound');
}
}
playGameOverSound() {
if (!gameProperties.isMuted && gameProperties.gameOverSound) {
gameProperties.gameOverSound.play();
console.log('Playing game over sound');
}
}
playDownSound() {
this.startAmbienceIfNeeded(); // Start ambience on first interaction
if (!gameProperties.isMuted && gameProperties.downSound) {
gameProperties.downSound.play();
console.log('Playing down sound');
}
}
playSplashSound() {
if (!gameProperties.isMuted && gameProperties.splashSound) {
gameProperties.splashSound.play();
console.log('Playing splash sound');
}
}
// Mute/Unmute functionality
setMuted(muted) {
gameProperties.isMuted = muted;
if (muted) {
// Stop all currently playing sounds
if (gameProperties.ambienceSound) {
gameProperties.ambienceSound.stop();
}
console.log('All sounds muted');
} else {
// Restart ambience if needed
if (gameProperties.gameStarted) {
this.startAmbienceIfNeeded();
}
console.log('Sounds unmuted');
}
}
// Improved airtime tracking for splash sound using terrain proximity
updateAirtimeTracking(time) {
if (!gameProperties.gameStarted || !this.birdSprite) return;
const shipX = this.birdSprite.x;
const shipY = this.birdSprite.y;
const bodyId = this.birdSprite.bodyId;
const velocity = b2Body_GetLinearVelocity(bodyId);
// Find the terrain height at the ship's current X position
const terrainHeightAtShip = this.getTerrainHeightAt(shipX);
// Consider ship "in air" if it's significantly above terrain AND has upward/neutral velocity
// This prevents false positives from small terrain bumps
const heightAboveTerrain = terrainHeightAtShip - shipY; // Higher values = more above terrain
const isSignificantlyAboveTerrain = heightAboveTerrain > 50; // Must be 50+ pixels above terrain
const hasUpwardOrNeutralVelocity = velocity.y <= 1; // Not falling too fast
const isCurrentlyInAir = isSignificantlyAboveTerrain && hasUpwardOrNeutralVelocity;
if (isCurrentlyInAir && !gameProperties.isInAir) {
// Just became airborne
gameProperties.isInAir = true;
gameProperties.airStartTime = time;
console.log(`Ship is now airborne (${heightAboveTerrain.toFixed(1)}px above terrain)`);
} else if (!isCurrentlyInAir && gameProperties.isInAir) {
// Just landed
const airtime = time - gameProperties.airStartTime;
console.log(`Ship landed after ${airtime}ms airtime (${heightAboveTerrain.toFixed(1)}px above terrain)`);
if (airtime >= gameProperties.minAirtimeForSplash) {
this.playSplashSound();
}
gameProperties.isInAir = false;
gameProperties.airStartTime = 0;
}
}
// Helper function to get terrain height at a specific X coordinate
getTerrainHeightAt(x) {
// Find the closest terrain points to the given X coordinate
let closestPoint = null;
let minDistance = Infinity;
gameProperties.terrainPoints.forEach(point => {
const distance = Math.abs(point.x - x);
if (distance < minDistance) {
minDistance = distance;
closestPoint = point;
}
});
// Return terrain height, or fallback to default if no terrain found
return closestPoint ? closestPoint.y : this.scale.height * gameConfig.terrainHeight;
}
// Restart game functionality for FarcadeSDK play_again
restartGame() {
console.log('Restarting game...');
// Reset game state variables
gameProperties.gameStarted = false;
gameProperties.gamePaused = false;
gameProperties.shipCaught = false;
gameProperties.isDiving = false;
gameProperties.isPoweredUp = false;
gameProperties.powerUpEndTime = 0;
gameProperties.coinScore = 0;
gameProperties.distanceTravelled = 0;
gameProperties.terrainSegmentCount = 0;
gameProperties.currentHeight = 0;
gameProperties.currentStack = -1;
gameProperties.maxHeightAchieved = 0;
// Reset audio state
gameProperties.ambienceStarted = false;
gameProperties.coinSequenceIndex = 0;
gameProperties.lastCoinCollectionTime = 0;
// Reset airtime tracking
gameProperties.isInAir = false;
gameProperties.airStartTime = 0;
gameProperties.terrainContactCount = 0;
// Stop all sounds
if (gameProperties.ambienceSound) {
gameProperties.ambienceSound.stop();
}
// Reset wave system
gameProperties.waveActive = false;
gameProperties.wavePosition = 0;
gameProperties.waveStartTime = 0;
// Clear terrain and objects
gameProperties.terrainSegments = [];
gameProperties.terrainPoints = [];
gameProperties.activeCoinGroups = [];
gameProperties.activePowerUps = [];
gameProperties.activeClouds = [];
gameProperties.activeMountains = [];
// Restart the scene
this.scene.restart();
}
initializeCoinPool() {
// Create a pool of 50 coins to reuse
for (let i = 0; i < 50; i++) {
const coin = this.createCoin(0, 0);
coin.setVisible(false);
coin.setActive(false);
gameProperties.coinPool.push(coin);
}
}
createCoin(x, y) {
const coin = this.add.image(x, y, 'coin');
coin.setDisplaySize(41, 35); // Display at 1x size (82x70 @ 2x = 41x35 @ 1x)
coin.setDepth(4); // Place coins above terrain layers
// Mark as coin for collision detection (no physics body needed for collection)
coin.setData('isCoin', true);
coin.setData('collected', false);
gameProperties.coinGroup.add(coin);
return coin;
}
getCoinFromPool() {
if (gameProperties.coinPool.length > 0) {
const coin = gameProperties.coinPool.pop();
coin.setVisible(true);
coin.setActive(true);
return coin;
}
// If pool is empty, create a new coin
return this.createCoin(0, 0);
}
returnCoinToPool(coin) {
coin.setVisible(false);
coin.setActive(false);
coin.setPosition(0, 0);
gameProperties.coinPool.push(coin);
}
initializePowerUpPool() {
// Create a pool of 10 power-ups to reuse
for (let i = 0; i < 10; i++) {
const powerUp = this.createPowerUp(0, 0);
powerUp.setVisible(false);
powerUp.setActive(false);
gameProperties.powerUpPool.push(powerUp);
}
}
createPowerUp(x, y) {
const powerUp = this.add.image(x, y, 'lightning');
powerUp.setDisplaySize(28, 39);
powerUp.setDepth(4); // Place power-ups above terrain layers
// Add golden glow effect
powerUp.setTint(0xffc937); // Golden tint
powerUp.setOrigin(0.5, 0.5);
powerUp.setData('isPowerUp', true);
powerUp.setData('collected', false);
powerUp.setData('originalY', y); // Store original Y for bobbing
gameProperties.powerUpGroup.add(powerUp);
return powerUp;
}
getPowerUpFromPool() {
if (gameProperties.powerUpPool.length > 0) {
const powerUp = gameProperties.powerUpPool.pop();
powerUp.setVisible(true);
powerUp.setActive(true);
// Restart bobbing animation when power-up is reused
const originalY = powerUp.getData('originalY') || powerUp.y;
this.tweens.add({
targets: powerUp,
y: originalY - 2, // Bob up 2 pixels
duration: 1500, // 1.5 seconds
ease: 'Sine.easeInOut',
yoyo: true, // Bob back down
repeat: -1 // Infinite loop
});
return powerUp;
}
return this.createPowerUp(0, 0);
}
returnPowerUpToPool(powerUp) {
powerUp.setVisible(false);
powerUp.setActive(false);
powerUp.setPosition(0, 0);
// Stop any active tween animations
this.tweens.killTweensOf(powerUp);
gameProperties.powerUpPool.push(powerUp);
}
initializeBackgroundPools() {
// Create cloud pool
for (let i = 0; i < this.cloudsForPool; i++) {
const cloud = this.createCloud(0, 0);
cloud.setVisible(false);
cloud.setActive(false);
gameProperties.cloudPool.push(cloud);
}
// Create mountain pool
for (let i = 0; i < this.mountainsForPool; i++) {
const mountain = this.createMountain(0, 0);
mountain.setVisible(false);
mountain.setActive(false);
gameProperties.mountainPool.push(mountain);
}
}
createCloud(x, y) {
const cloud = this.add.image(x, y, 'cloud');
cloud.setDepth(0);
cloud.setScrollFactor(0, 1);
gameProperties.cloudGroup.add(cloud);
return cloud;
}
createMountain(x, y) {
const mountain = this.add.image(x, y, 'mountain');
mountain.setDepth(-1);
mountain.setScrollFactor(0, 1);
gameProperties.mountainGroup.add(mountain);
return mountain;
}
getCloudFromPool() {
if (gameProperties.cloudPool.length > 0) {
const cloud = gameProperties.cloudPool.pop();
cloud.setVisible(true);
cloud.setActive(true);
return cloud;
}
return this.createCloud(0, 0);
}
getMountainFromPool() {
if (gameProperties.mountainPool.length > 0) {
const mountain = gameProperties.mountainPool.pop();
mountain.setVisible(true);
mountain.setActive(true);
return mountain;
}
return this.createMountain(0, 0);
}
returnCloudToPool(cloud) {
cloud.setVisible(false);
cloud.setActive(false);
cloud.setPosition(0, 0);
gameProperties.cloudPool.push(cloud);
}
returnMountainToPool(mountain) {
mountain.setVisible(false);
mountain.setActive(false);
mountain.setPosition(0, 0);
gameProperties.mountainPool.push(mountain);
}
initializeParticleSystem() {
// Create glowing particle texture
const particleGraphics = this.add.graphics();
particleGraphics.fillStyle(0xFFFFFF); // White for tinting
particleGraphics.fillCircle(0, 0, 4);
particleGraphics.generateTexture('glowParticle', 8, 8);
particleGraphics.destroy();
// Create particle emitter for power-up trail effect
gameProperties.particleEmitter = this.add.particles(0, 0, 'glowParticle', {
speed: { min: 80, max: 120 },
scale: { start: 2, end: 0.2 },
alpha: { start: 1, end: 0 },
lifespan: 600,
frequency: 30, // Emit particles frequently for dense trail
quantity: 5, // Multiple particles per emission
tint: [0xffc937, 0xffed4e, 0xffeb3b], // Golden variations
angle: { min: 160, max: 200 }, // Emit backwards from bird
gravityY: -20, // Slight upward drift
blendMode: 'ADD', // Additive blending for glow effect
emitting: false
});
gameProperties.particleEmitter.setDepth(5); // Above terrain and items
// Create secondary emitter for extra glow
gameProperties.particleEmitterGlow = this.add.particles(0, 0, 'glowParticle', {
speed: { min: 40, max: 80 },
scale: { start: 3, end: 0 },
alpha: { start: 0.6, end: 0 },
lifespan: 400,
frequency: 50,
quantity: 2,
tint: 0xffc937, // Golden glow
angle: { min: 150, max: 210 },
gravityY: -10,
blendMode: 'ADD',
emitting: false
});
gameProperties.particleEmitterGlow.setDepth(5); // Above terrain and items
}
generateCoinGroup(terrainData) {
const coinCount = Phaser.Math.Between(gameConfig.coinsPerGroup.min, gameConfig.coinsPerGroup.max);
const coinGroup = [];
// Calculate total width needed for coins with gaps
const coinDiameter = gameConfig.coinRadius * 2;
const totalGapWidth = (coinCount - 1) * gameConfig.coinGap;
const totalCoinsWidth = coinCount * coinDiameter;
const totalGroupWidth = totalCoinsWidth + totalGapWidth;
// Find the center point of the dip (where the terrain is lowest)
const curveWidth = terrainData.endX - terrainData.startX;
const dipCenterX = terrainData.centerPoint.x;
// Calculate starting position to center the group
const groupStartX = dipCenterX - (totalGroupWidth / 2);
// Place coins following the terrain curvature
for (let i = 0; i < coinCount; i++) {
const coin = this.getCoinFromPool();
// Calculate this coin's X position
const coinX = groupStartX + (i * (coinDiameter + gameConfig.coinGap)) + gameConfig.coinRadius;
// Calculate how far this X position is along the terrain curve
const relativeX = coinX - terrainData.startX;
const curveProgress = Math.max(0, Math.min(1, relativeX / curveWidth)); // Clamp between 0-1
// Use terrain curve equation to get Y position
const segmentPosition = curveProgress * terrainData.segments;
const cosine = -Math.cos(Math.PI / terrainData.segments * segmentPosition);
const coinY = terrainData.centerPoint.y + (terrainData.lineHeight * cosine) - gameConfig.coinHeightOffset;
coin.setPosition(coinX, coinY);
coin.setData('collected', false); // Reset collection state
coinGroup.push(coin);
}
// Track this coin group for cleanup
gameProperties.activeCoinGroups.push({
coins: coinGroup,
centerX: terrainData.centerPoint.x
});
}
generatePowerUp({ lowestPoint }) {
const powerUp = this.getPowerUpFromPool();
const powerUpX = lowestPoint.x;
const powerUpY = lowestPoint.y - gameConfig.powerUpHeightOffset;
this.tweens.killTweensOf(powerUp);
// Create gentle bobbing animation
this.tweens.add({
targets: powerUp,
y: powerUpY - 2, // Bob up 2 pixels
duration: 1500, // 1.5 seconds
ease: 'Sine.easeInOut',
yoyo: true, // Bob back down
repeat: -1 // Infinite loop
});
powerUp.setPosition(powerUpX, powerUpY);
powerUp.setData('collected', false);
powerUp.setData('originalY', powerUpY); // For bobbing
gameProperties.activePowerUps.push({
powerUp,
centerX: powerUpX
});
}
generateBackgroundClouds(terrainCenterX) {
if (Math.random() < gameConfig.cloudSpawnChance) {
// Reuse oldest cloud if all are active
if (gameProperties.activeClouds.length >= this.cloudsForPool) {
const oldestCloud = gameProperties.activeClouds.shift();
this.returnCloudToPool(oldestCloud.sprite);
}
// Get cloud from pool
const cloud = this.getCloudFromPool();
// Random position within the terrain segment width
const cloudX = terrainCenterX + Phaser.Math.Between(-200, 200);
const cloudY = Phaser.Math.Between(20, gameConfig.cloudTopAreaHeight - 20);
const cloudScale = Phaser.Math.FloatBetween(gameConfig.cloudMinScale, gameConfig.cloudMaxScale);
cloud.setPosition(cloudX, cloudY);
cloud.setDisplaySize(145 * cloudScale, 67 * cloudScale);
gameProperties.activeClouds.push({
sprite: cloud,
originalX: cloudX
});
}
}
generateBackgroundMountains(terrainCenterX) {
// RNG gate – keep your original spawn chance
if (Math.random() >= gameConfig.mountainSpawnChance) return;
/*--------------------------------------------------------------
Try a handful of random positions until we find one that
does NOT overlap any existing mountain. Bail out if none
work (very unlikely unless spawn density is huge).
--------------------------------------------------------------*/
const MAX_ATTEMPTS = 8;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
// 1) random size & position
const scale = Phaser.Math.FloatBetween(
gameConfig.mountainMinScale,
gameConfig.mountainMaxScale
);
const width = 185 * scale; // your texture’s 1× width
const halfW = width * 0.5;
const mountainX = terrainCenterX +
Phaser.Math.Between(-300, 300); // ± 300 px
const mountainY = gameConfig.mountainBottomY -
(185 * scale) * 0.5; // keep bottom anchored
// 2) overlap check against *current* mountains
const overlaps = gameProperties.activeMountains.some(({ sprite }) => {
const otherHalf = sprite.displayWidth * 0.5;
return Math.abs(sprite.x - mountainX) < (halfW + otherHalf);
});
if (overlaps) continue; // try another random spot
/*----------------------------------------------------------
This spot is clear – create the sprite.
----------------------------------------------------------*/
// Reuse oldest mountain if all are active
if (gameProperties.activeMountains.length >= this.mountainsForPool) {
const oldestMountain = gameProperties.activeMountains.shift();
this.returnMountainToPool(oldestMountain.sprite);
}
// Get mountain from pool
const mountain = this.getMountainFromPool();
mountain.setPosition(mountainX, mountainY);
mountain.setDisplaySize(width, 185 * scale);
mountain.setFlipX(Math.random() < 0.5); // 50% chance to mirror
gameProperties.activeMountains.push({
sprite: mountain,
originalX: mountainX // keep for parallax calc
});
break; // we spawned one – exit the loop
}
}
updateShipRotation() {
if (!gameProperties.gameStarted || !this.birdSprite) return;
const bird = this.birdSprite;
const bodyId = bird.bodyId;
// --- current velocity -----------------------------------------------
const v = b2Body_GetLinearVelocity(bodyId);
// --- accumulate with the previous 5 ---------------------------------
let wx = v.x, wy = v.y;
for (let i = 0; i < gameProperties.maxPrevVelocities; i++) {
wx += gameProperties.prevVelocities[i].x;
wy += gameProperties.prevVelocities[i].y;
}
// we used (maxPrevVelocities + 1) samples
const samples = gameProperties.maxPrevVelocities + 1;
wx /= samples;
wy /= samples;
// store the current velocity for the next frame (circular buffer)
gameProperties.prevVelocities[gameProperties.nextVelIndex] = { x: v.x, y: v.y };
gameProperties.nextVelIndex = (gameProperties.nextVelIndex + 1) % gameProperties.maxPrevVelocities;
// --- optional guard: skip if almost standing still -------------------
if (Math.abs(wx) + Math.abs(wy) < 0.05) return;
// --- set the visual lean *once* -------------------------------------
const lean = Math.atan2(wy, wx); // radians
bird.rotation = -lean; // Phaser wants radians
}
updateParallaxBackground() {
if (!gameProperties.gameStarted) {
gameProperties.activeClouds.forEach(cloudData => {
cloudData.sprite.x = cloudData.sprite.x - 0.25;
});
return;
}
const cameraScrollX = this.cameras.main.scrollX;
// Update cloud positions (slower parallax - farther away)
gameProperties.activeClouds.forEach(cloudData => {
const parallaxOffset = cameraScrollX * gameConfig.cloudParallaxSpeed;
cloudData.sprite.x = cloudData.originalX - parallaxOffset;
});
// Update mountain positions (faster parallax - closer)
gameProperties.activeMountains.forEach(mountainData => {
const parallaxOffset = cameraScrollX * gameConfig.mountainParallaxSpeed;
mountainData.sprite.x = mountainData.originalX - parallaxOffset;
});
}
initializePhysicsWorld() {
SetWorldScale(20);
const worldDef = b2DefaultWorldDef();
worldDef.gravity.y = gameConfig.gravityY;
this.world = CreateWorld({ worldDef });
this.worldId = this.world.worldId;
this.debug = this.add.graphics().setVisible(false);
}
initializeGameElements() {
this.addInput();
this.generateTextures();
const birdStartPosition = this.generateTerrain({ x: gameConfig.terrainStartX, y: this.scale.height * gameConfig.terrainHeight });
this.birdSprite = this.addBird(birdStartPosition);
this.landingSprite = this.addLanding({ x: 75, y: this.scale.height * gameConfig.terrainHeight });
this.createWaveSprite();
}
createWaveSprite() {
// Create the visual wave sprite that chases the ship
// wave.png is 1472x1212 @ 2x, so 736x606 @ 1x
// We want 700px tall, so calculate width: (700/606) * 736 = 850px width too (square image)
const waveHeight = 900;
const waveWidth = 1098;
// Position wave off-screen initially, bottom aligned with screen bottom
this.waveSprite = this.add.image(-500, 600, 'wave');
this.waveSprite.setDisplaySize(waveWidth, waveHeight);
this.waveSprite.setDepth(6); // Above terrain layers (terrain is depth 1-2, coins/powerups are 4)
this.waveSprite.setVisible(false); // Hidden until wave starts chasing
}
initCamera() {
this.cameras.main.setOrigin(0, 0.5);
}
addInput() {
// Mouse/touch input (works for both mouse clicks and touch on mobile)
this.input.on('pointerdown', this.pointerDown, this);
this.input.on('pointerup', this.pointerUp, this);
// Keyboard input - spacebar for diving
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
// Handle spacebar press and release
this.spaceKey.on('down', this.pointerDown, this);
this.spaceKey.on('up', this.pointerUp, this);
}
addBlueBackground() {
const w = this.scale.width;
const h = this.scale.height;
// 1. draw the gradient once
const bg = this.add.graphics();
bg.fillGradientStyle(
gameConfig.skyColorTop, // TL & TR
gameConfig.skyColorTop,
gameConfig.skyColorBottom, // BL & BR
gameConfig.skyColorBottom
);
bg.fillRect(0, 0, w, h);
// 2. camera that will only render the gradient
const bgCam = this.cameras.add(0, 0, w, h)
.setZoom(1)
.setScroll(0, 0);
// 3. exclusion masks
this.cameras.main.ignore(bg); // main camera skips gradient
bgCam.ignore(this.children.list.filter(o => o !== bg)); // bg camera shows nothing but the gradient
// 4. move bgCam to index 0 (rendered first = behind everything else)
const list = this.cameras.cameras; // internal array
list.splice(list.indexOf(bgCam), 1); // remove from current pos (end)
list.unshift(bgCam); // put at front
// 5. keep it fitted on resize
this.scale.on('resize', ({ width, height }) => {
bgCam.setSize(width, height);
this.redrawGradient(bg, width, height);
});
}
redrawGradient(g, w, h) {
g.clear();
g.fillGradientStyle(
gameConfig.skyColorTop, gameConfig.skyColorTop,
gameConfig.skyColorBottom, gameConfig.skyColorBottom
);
g.fillRect(0, 0, w, h);
}
limitVelocity() {
if (!gameProperties.gameStarted) return;
const bird = this.birdSprite;
const bodyId = bird.bodyId;
// Get current velocity
const velocity = b2Body_GetLinearVelocity(bodyId);
// Use power-up velocity if active, otherwise use normal minimum
const minVelX = gameProperties.isPoweredUp ? gameConfig.powerUpVelocityBoost : gameConfig.minVelocityX;
// Ensure minimum forward velocity (prevents rolling backward)
let newVelX = velocity.x;
let newVelY = velocity.y;
if (newVelX < minVelX) {
newVelX = minVelX;
}
if (newVelY < gameConfig.minVelocityY) {
newVelY = gameConfig.minVelocityY;
}
// Set the clamped velocity back to the body
if (newVelX !== velocity.x || newVelY !== velocity.y) {
b2Body_SetLinearVelocity(bodyId, new b2Vec2(newVelX, newVelY));
}
}
fireBird() {
if (gameProperties.gameStarted) return;
gameProperties.gameStarted = true;
// Start ambience music when game begins
this.startAmbienceIfNeeded();
// Show the UI elements now that the game has started
if (this.GameUi && this.GameUi.showGameUI) {
this.GameUi.showGameUI();
}
/*const bird = this.birdSprite;
const bodyId = bird.bodyId;
const force = b2MulSV(gameConfig.birdFireForce, gameConfig.birdFireDirection);
b2Body_ApplyForceToCenter(bodyId, force, true);*/
}
// Calculate current difficulty based on distance traveled
getDifficultyMultiplier() {
const distance = gameProperties.distanceTravelled || 0;
const maxDistance = gameConfig.difficulty.maxDifficultyDistance;
// Returns value from 0 (start) to 1 (max difficulty)
return Math.min(distance / maxDistance, 1);
}
// Get current wave speed based on difficulty progression
getCurrentWaveSpeed() {
const difficulty = this.getDifficultyMultiplier();
const minSpeed = gameConfig.difficulty.waveSpeedMin;
const maxSpeed = gameConfig.difficulty.waveSpeedMax;
return minSpeed + (maxSpeed - minSpeed) * difficulty;
}
// Get current terrain amplitude based on difficulty progression
getCurrentTerrainAmplitude() {
const difficulty = this.getDifficultyMultiplier();
const startAmp = gameConfig.difficulty.terrainAmplitudeStart;
const endMinAmp = gameConfig.difficulty.terrainAmplitudeEndMin;
const endMaxAmp = gameConfig.difficulty.terrainAmplitudeEndMax;
// Apply smooth easing curve for more gradual progression
// Use ease-in-out curve: starts slow, accelerates in middle, slows at end
const easedDifficulty = (1 - Math.cos(difficulty * Math.PI)) / 2;
// Interpolate between start and end values using the eased curve
const currentMin = startAmp + (endMinAmp - startAmp) * easedDifficulty;
const currentMax = startAmp + (endMaxAmp - startAmp) * easedDifficulty;
return {
min: Math.round(currentMin),
max: Math.round(currentMax)
};
}
// Helper function to determine correct ship frame based on states
getCorrectShipFrame() {
if (gameProperties.isPoweredUp) {
return 2; // Power-up frame takes priority
}
return gameProperties.isDiving ? 1 : 0; // Normal diving logic
}
pointerDown() {
gameProperties.isDiving = true;
// Play down sound
this.playDownSound();
if (this.birdSprite) {
this.birdSprite.setFrame(this.getCorrectShipFrame());
}
}
pointerUp() {
gameProperties.isDiving = false;
if (this.birdSprite) {
this.birdSprite.setFrame(this.getCorrectShipFrame());
}
}
generateTextures() {
buildGrainTexture(
this, // scene
'terrainLayer2Pattern', // key
gameConfig.terrainLayer2Color,
0.05, // intensity 15 %
0.25, // contrast 44 %
64 // 64×64 is plenty
);
buildGrainTexture(
this,
'terrainLayer3Pattern',
gameConfig.terrainLayer3Color,
0.05, // heavier grain for bottom
0.25,
64
);
// Create texture for top terrain line
const terrainLineGraphics = this.add.graphics().fillStyle(gameConfig.terrainLineColor).fillRect(0, 0, 100, 100);
terrainLineGraphics.generateTexture('terrainLine', 100, 100);
terrainLineGraphics.destroy();
// Keep legacy ground texture for physics bodies (invisible)
const terrainGraphics = this.add.graphics().fillStyle(gameConfig.terrainColor).fillRect(0, 0, 100, 100);
terrainGraphics.generateTexture('ground', 100, 100);
terrainGraphics.destroy();
}
generateTerrain(startPoint) {
let terrainX = startPoint.x;
let terrainY = startPoint.y;
let terrainPoints = [];
// Bounds for terrain Y position
const minY = 100;
const maxY = 500;
const centerY = (minY + maxY) / 2; // 300
const boundsBuffer = 80; // Distance from bounds to start biasing back
for (let i = 0; i < gameConfig.terrainToGenerate; i++) {
gameProperties.terrainSegmentCount++;
const terrainLength = Phaser.Math.Between(gameConfig.terrainSegmentsMin, gameConfig.terrainSegmentsMax) * gameConfig.terrainSegmentLength;
// Use dynamic terrain amplitude based on difficulty progression
const currentAmplitude = this.getCurrentTerrainAmplitude();
let terrainAmplitude = Phaser.Math.Between(currentAmplitude.min, currentAmplitude.max) * gameConfig.terrainAmplitudeSize;
// Debug: Log terrain difficulty progression occasionally
if (gameProperties.terrainSegmentCount % 50 === 0) {
const difficultyPercent = (this.getDifficultyMultiplier() * 100).toFixed(1);
console.log(`Terrain Difficulty: ${difficultyPercent}% | Amplitude Range: ${currentAmplitude.min}-${currentAmplitude.max} | Distance: ${Math.round(gameProperties.distanceTravelled || 0)}`);
}
// Calculate bias toward center when near bounds
let yBias = 0;
if (terrainY < minY + boundsBuffer) {
// Too close to bottom, bias upward
const distanceFromBound = terrainY - minY;
yBias = (boundsBuffer - distanceFromBound) * 2; // Stronger bias closer to bound
} else if (terrainY > maxY - boundsBuffer) {
// Too close to top, bias downward
const distanceFromBound = maxY - terrainY;
yBias = -(boundsBuffer - distanceFromBound) * 2; // Negative bias for downward
}
const startPoint = {
x: terrainX,
y: i === 0 ? terrainY : Math.round(terrainY - terrainAmplitude + yBias)
}
terrainX += terrainLength;
const endPoint = {
x: terrainX,
y: i === 0 ? terrainY + (gameConfig.terrainAmplitudeMax * gameConfig.terrainAmplitudeSize) : Math.round(terrainY + terrainAmplitude + yBias)
}
// Update terrainY to track the actual end position for next iteration
terrainY = endPoint.y;
// Clamp terrainY to absolute bounds
terrainY = Math.max(minY, Math.min(maxY, terrainY));
console.log(startPoint, endPoint);
if (i === gameConfig.terrainToGenerate - 1) {
terrainPoints.push(startPoint);
}
else {
terrainPoints.push(startPoint, endPoint);
}
terrainX += terrainLength;
}
gameProperties.lastPoint = terrainPoints[terrainPoints.length - 1];
gameProperties.distanceToNextTerrain = terrainPoints[1].x;
let currentPoint = null;
let lastPoint = null;
if (!gameProperties.gameStarted) this.addSegment(terrainPoints[0].x - gameConfig.terrainStartX, terrainPoints[0].y, terrainPoints[0].x, terrainPoints[0].y);
for (let i = 1; i < terrainPoints.length; i++) {
const startPoint = terrainPoints[i - 1];
const endPoint = terrainPoints[i];
const lineWidth = endPoint.x - startPoint.x;
const lineHeight = (endPoint.y - startPoint.y) * .5;
const segments = lineWidth / gameConfig.terrainSegmentLength;
const terrainLine = new Phaser.Geom.Line(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
const centerPoint = Phaser.Geom.Line.GetPoint(terrainLine, 0.5);
currentPoint = lastPoint || startPoint;
let lowestPoint = { x: 0, y: 0 };
let hasLowPoint = false;
for (let j = 0; j < segments; j++) {
const sine = Math.sin(Math.PI / segments * j);
const cosine = -Math.cos(Math.PI / segments * j);
const nextPoint =
{
x: currentPoint.x + gameConfig.terrainSegmentLength,
y: centerPoint.y + (lineHeight * cosine)
}
this.addSegment(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y);
// Track the lowest point for coin placement
if (!hasLowPoint || nextPoint.y > lowestPoint.y) {
lowestPoint = { x: nextPoint.x, y: nextPoint.y };
hasLowPoint = true;
}
currentPoint = nextPoint;
if (j === segments - 1) {
lastPoint = nextPoint;
}
}
// Generate items only after initial segments
if (gameProperties.terrainSegmentCount > gameConfig.noSpawnSegments && hasLowPoint) {
// Generate coins if RNG allows
if (Math.random() < gameConfig.coinSpawnChance) {
this.generateCoinGroup({
centerPoint: centerPoint,
lineHeight: lineHeight,
segments: segments,
startX: startPoint.x,
endX: endPoint.x
});
}
// Generate power-up if RNG allows (separate from coins)
else if (Math.random() < gameConfig.powerUpSpawnChance) {
this.generatePowerUp({
lowestPoint: lowestPoint,
});
}
}
// Generate background elements for this terrain segment
this.generateBackgroundClouds(centerPoint.x);
this.generateBackgroundMountains(centerPoint.x);
}
// Redraw terrain layers with all new points
this.redrawTerrainLayers();
const startLine = new Phaser.Geom.Line(terrainPoints[0].x - gameConfig.terrainStartX, terrainPoints[0].y, terrainPoints[0].x, terrainPoints[0].y);
const birdStartPosition = Phaser.Geom.Line.GetPoint(startLine, 0.9)
birdStartPosition.y -= 50;
if (!gameProperties.gameStarted) return birdStartPosition;
else return null;
}
addSegment(x1, y1, x2, y2) {
const segmentLine = new Phaser.Geom.Line(x1, y1, x2, y2);
const segmentLength = Phaser.Geom.Line.Length(segmentLine);
const centerPoint = Phaser.Geom.Line.GetPoint(segmentLine, 0.5);
const angle = Phaser.Geom.Line.Angle(segmentLine);
// Store terrain points for smooth graphics rendering
gameProperties.terrainPoints.push({ x: x1, y: y1 });
gameProperties.terrainPoints.push({ x: x2, y: y2 });
// Create top terrain line (4px thick) - visible collision line
const topLine = this.add.image(centerPoint.x, centerPoint.y, 'terrainLine')
.setDisplaySize(segmentLength, gameConfig.terrainLineThickness)
.setAngle(Phaser.Math.RadToDeg(angle))
.setDepth(3);
// Create invisible physics body using the top line position
const physicsSegment = this.add.image(centerPoint.x, centerPoint.y, 'ground')
.setDisplaySize(segmentLength, gameConfig.terrainLineThickness)
.setAngle(Phaser.Math.RadToDeg(angle))
.setVisible(false); // Invisible - only for physics
gameProperties.terrainSegments.push({
topLine: topLine,
physics: physicsSegment,
points: [{ x: x1, y: y1 }, { x: x2, y: y2 }]
});
const ground = SpriteToBox(this.worldId, physicsSegment, {
type: STATIC,
friction: 1,
restitution: 0,
});
physicsSegment.setData('ground-bodyId', ground.bodyId);
gameProperties.segmentGroup.add(topLine);
gameProperties.segmentGroup.add(physicsSegment);
}
redrawTerrainLayers() {
/*──────────────── build point list ───────────────*/
const pts = gameProperties.terrainPoints;
if (pts.length < 4) return;
const unique = [...pts]
.sort((a, b) => a.x - b.x)
.filter((p, i, arr) => i === 0 || p.x !== arr[i - 1].x);
if (unique.length < 2) return;
const g2 = gameProperties.terrainLayer2Graphics;
const g3 = gameProperties.terrainLayer3Graphics;
/*──────────────── helpers ────────────────────────*/
const firstX = unique[0].x;
const lastX = unique[unique.length - 1].x;
const LEFT_PAD = 400; // slack on the left
const RIGHT_PAD = 600; // slack on the right
const leftEdge = firstX - LEFT_PAD; // new sprite x‑pos
const neededW = (lastX - firstX) + LEFT_PAD + RIGHT_PAD;
const bandH = this.scale.height + 800;
const fillH = this.scale.height + 800;
/*──────────────── 1) bottom‑fill mask ───────────*/
g3.clear().beginPath();
g3.moveTo(firstX, unique[0].y);
for (let i = 1; i < unique.length; i++) g3.lineTo(unique[i].x, unique[i].y);
g3.lineTo(lastX, this.scale.height + 600);
g3.lineTo(firstX, this.scale.height + 600);
g3.closePath().fillStyle(0xffffff).fillPath().setVisible(false);
const mask3 = g3.createGeometryMask();
/*──────────────── 2) 50 px band mask ────────────*/
g2.clear().beginPath();
g2.moveTo(firstX, unique[0].y);
for (let i = 1; i < unique.length; i++) g2.lineTo(unique[i].x, unique[i].y);
for (let i = unique.length - 1; i >= 0; i--)
g2.lineTo(unique[i].x, unique[i].y + gameConfig.terrainLayer2Thickness);
g2.closePath().fillStyle(0xffffff).fillPath().setVisible(false);
const mask2 = g2.createGeometryMask();
/*──────────────── bottom layer sprite ───────────*/
if (!this._layer3Tile) {
this._layer3Tile = this.add.tileSprite(leftEdge, 0, neededW, fillH,
'terrainLayer3Pattern')
.setOrigin(0).setDepth(1).setScrollFactor(1);
}
this._layer3Tile.setMask(mask3);
if (this._layer3Tile.width < neededW) this._layer3Tile.setSize(neededW, fillH);
if (this._layer3Tile.x !== leftEdge) this._layer3Tile.x = leftEdge;
/*──────────────── 50 px band sprite ─────────────*/
if (!this._layer2Tile) {
this._layer2Tile = this.add.tileSprite(leftEdge, 0, neededW, bandH,
'terrainLayer2Pattern')
.setOrigin(0).setDepth(2).setScrollFactor(1);
}
this._layer2Tile.setMask(mask2);
if (this._layer2Tile.width < neededW) this._layer2Tile.setSize(neededW, bandH);
if (this._layer2Tile.x !== leftEdge) this._layer2Tile.x = leftEdge;
// Optional parallax:
// this._layer3Tile.tilePositionX = this.cameras.main.scrollX * 0.4;
// this._layer2Tile.tilePositionX = this.cameras.main.scrollX * 0.6;
}
removeSegments() {
if (!gameProperties.gameStarted) return;
let segmentsRemoved = false;
// Filter terrain segments and remove those that are too far behind
gameProperties.terrainSegments = gameProperties.terrainSegments.filter((segmentData) => {
const dx = this.birdSprite.x - segmentData.physics.x;
if (dx > gameConfig.removeSegmentX) {
// Remove visual elements and physics body
const ground = segmentData.physics.getData('ground-bodyId');
gameProperties.segmentGroup.remove(segmentData.topLine, true, true);
gameProperties.segmentGroup.remove(segmentData.physics, true, true);
RemoveSpriteFromWorld(this.worldId, segmentData.physics);
if (ground) {
b2DestroyBody(ground);
}
// Remove associated terrain points
segmentData.points.forEach(point => {
const pointIndex = gameProperties.terrainPoints.findIndex(p => p.x === point.x && p.y === point.y);
if (pointIndex !== -1) {
gameProperties.terrainPoints.splice(pointIndex, 1);
}
});
segmentsRemoved = true;
return false; // Remove from array
}
return true; // Keep in array
});
// Redraw terrain layers if segments were removed
if (segmentsRemoved) {
this.redrawTerrainLayers();
}
}
cleanupCoins() {
if (!gameProperties.gameStarted) return;
// Remove coin groups that are far behind the bird
gameProperties.activeCoinGroups = gameProperties.activeCoinGroups.filter(coinGroup => {
const dx = this.birdSprite.x - coinGroup.centerX;
if (dx > gameConfig.removeSegmentX) {
// Return coins to pool
coinGroup.coins.forEach(coin => {
this.returnCoinToPool(coin);
});
return false; // Remove this group
}
return true; // Keep this group
});
}
cleanupPowerUps() {
if (!gameProperties.gameStarted) return;
// Remove power-ups that are far behind the bird
gameProperties.activePowerUps = gameProperties.activePowerUps.filter(powerUpData => {
const dx = this.birdSprite.x - powerUpData.centerX;
if (dx > gameConfig.removeSegmentX) {
// Return power-up to pool
this.returnPowerUpToPool(powerUpData.powerUp);
return false; // Remove this power-up
}
return true; // Keep this power-up
});
}
checkCoinCollection() {
if (!gameProperties.gameStarted) return;
const birdX = this.birdSprite.x;
const birdY = this.birdSprite.y;
gameProperties.activeCoinGroups.forEach(coinGroup => {
coinGroup.coins.forEach(coin => {
if (coin.active && coin.visible && !coin.getData('collected')) {
const dx = birdX - coin.x;
const dy = birdY - coin.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < gameConfig.coinCollectionDistance) {
// Collect the coin
coin.setData('collected', true);
coin.setVisible(false);
coin.setActive(false);
// Add points to coin score
gameProperties.coinScore += gameConfig.coinPoints;
// Play coin collection sound
this.playCoinCollectionSound();
// Visual feedback (optional - could add particle effect here)
console.log('Coin collected! +' + gameConfig.coinPoints + ' points');
}
}
});
});
}
checkPowerUpCollection() {
if (!gameProperties.gameStarted) return;
const birdX = this.birdSprite.x;
const birdY = this.birdSprite.y;
gameProperties.activePowerUps.forEach((powerUpData, index) => {
const powerUp = powerUpData.powerUp;
if (powerUp.active && powerUp.visible && !powerUp.getData('collected')) {
const dx = birdX - powerUp.x;
const dy = birdY - powerUp.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < gameConfig.powerUpCollectionDistance) {
// Collect the power-up
powerUp.setData('collected', true);
powerUp.setVisible(false);
powerUp.setActive(false);
// Play thruster sound
this.playThrusterSound();
// Activate power-up effect
this.activatePowerUp();
console.log('Power-up collected! Speed boost activated!');
}
}
});
}
activatePowerUp() {
gameProperties.isPoweredUp = true;
gameProperties.powerUpEndTime = Date.now() + gameConfig.powerUpDuration;
// Switch to powered-up ship sprite (frame 2 - power-up sprite)
const bird = this.birdSprite;
bird.setFrame(this.getCorrectShipFrame());
// Boost velocity immediately
const bodyId = bird.bodyId;
const currentVel = b2Body_GetLinearVelocity(bodyId);
b2Body_SetLinearVelocity(bodyId, new b2Vec2(gameConfig.powerUpVelocityBoost, currentVel.y));
// Start particle trail effects
if (gameProperties.particleEmitter) {
gameProperties.particleEmitter.setPosition(bird.x, bird.y);
gameProperties.particleEmitter.start();
}
if (gameProperties.particleEmitterGlow) {
gameProperties.particleEmitterGlow.setPosition(bird.x, bird.y);
gameProperties.particleEmitterGlow.start();
}
}
updatePowerUpEffects() {
if (!gameProperties.gameStarted || !gameProperties.isPoweredUp) return;
// Update particle emitter positions to follow behind the bird
if (this.birdSprite) {
const birdX = this.birdSprite.x;
const birdY = this.birdSprite.y;
const ox = -this.birdSprite.displayWidth * 0.5;
const oy = this.birdSprite.displayHeight * 0.5;
// Position emitters at the back of the ship for trailing effect
if (gameProperties.particleEmitter) {
gameProperties.particleEmitter.setPosition(birdX + ox, birdY + oy);
}
if (gameProperties.particleEmitterGlow) {
gameProperties.particleEmitterGlow.setPosition(birdX + ox, birdY + oy);
}
}
// Check if power-up duration has ended
if (Date.now() >= gameProperties.powerUpEndTime) {
gameProperties.isPoweredUp = false;
// Switch back to correct ship sprite based on current diving state
if (this.birdSprite) {
this.birdSprite.setFrame(this.getCorrectShipFrame());
}
// Stop both particle effects
if (gameProperties.particleEmitter) {
gameProperties.particleEmitter.stop();
}
if (gameProperties.particleEmitterGlow) {
gameProperties.particleEmitterGlow.stop();
}
console.log('Power-up effect ended');
}
}
updateCamera() {
// Follow bird horizontally but keep more distance for portrait view
this.cameras.main.scrollX = this.birdSprite.x - 150;
// More zoomed out camera for better overview in portrait mode
const birdHeight = (this.scale.height - this.birdSprite.y);
const zoomFactor = Math.min(this.scale.height / birdHeight, 0.7); // Reduced from 1.5 to 0.7
this.cameras.main.scrollY = Math.min((this.scale.height * 0.4) - (birdHeight * 0.5), 0);
this.cameras.main.zoomTo(zoomFactor, gameConfig.cameraTransitionTime);
// Only generate new terrain if game is not paused
if (!gameProperties.gamePaused && gameProperties.distanceTravelled > gameProperties.distanceToNextTerrain) {
this.generateTerrain(gameProperties.lastPoint);
}
}
updateScore() {
if (!gameProperties.gameStarted) return;
gameProperties.distanceTravelled = this.birdSprite.x - gameProperties.birdStartX;
// Distance score (like Tiny Wings) + coin bonuses
const distanceScore = Math.floor(gameProperties.distanceTravelled / 20);
gameProperties.score = distanceScore + gameProperties.coinScore;
this.GameUi.updateScore(gameProperties.score);
}
addLanding(pointer) {
const landingSprite = this.add.sprite(pointer.x, pointer.y, 'landing');
landingSprite.setDisplaySize(181, 121);
landingSprite.setDepth(0);
return landingSprite;
}
addBird(pointer) {
gameProperties.birdStartX = pointer.x;
const pointerX = pointer.x;
const pointerY = pointer.y;
// Create ship sprite (default to frame 0 - left sprite)
const birdSprite = this.add.sprite(pointerX, pointerY, 'ship', 0);
birdSprite.setDisplaySize(93, 83); // Scale down from 2x asset (186/2, 166/2)
birdSprite.setDepth(6); // Above all other elements
// Debug: Check how many frames are available
const texture = this.textures.get('ship');
const frameNames = texture.getFrameNames();
console.log('Available ship frames:', frameNames, 'Total frames:', frameNames.length);
const bird = SpriteToCircle(this.worldId, birdSprite, {
restitution: 0,
friction: 0.5,
angularDamping: 1,
radius: 1
});
birdSprite.bodyId = bird.bodyId;
AddSpriteToWorld(this.worldId, birdSprite, bird);
return birdSprite;
}
birdDive() {
if (!gameProperties.isDiving || !gameProperties.gameStarted) return;
const bird = this.birdSprite;
const bodyId = bird.bodyId;
const force = b2MulSV(gameConfig.birdDiveForce, gameConfig.birdDiveDirection);
b2Body_ApplyForceToCenter(bodyId, force, true);
}
initializeWave() {
// Initialize wave position when game starts
if (gameProperties.gameStarted && !gameProperties.waveActive) {
const shipX = this.birdSprite ? this.birdSprite.x : 250;
gameProperties.wavePosition = shipX - gameConfig.waveInitialDistance;
console.log('initializeWave wavePosition', gameProperties.wavePosition);
gameProperties.waveStartTime = Date.now() + gameConfig.waveStartDelay;
gameProperties.waveActive = true;
}
}
updateWaveChasing(time) {
// Only start wave after game has started and delay has passed
if (!gameProperties.gameStarted || !gameProperties.waveActive) {
this.initializeWave();
return;
}
if (Date.now() < gameProperties.waveStartTime) {
return; // Still in delay period
}
// Make wave sprite visible once chasing starts
if (this.waveSprite && !this.waveSprite.visible) {
this.waveSprite.setVisible(true);
}
// Get current ship position
const shipX = this.birdSprite ? this.birdSprite.x : 250;
// Move wave forward constantly (unless game is paused)
// Use dynamic wave speed that increases with difficulty
const currentWaveSpeed = this.getCurrentWaveSpeed();
if (!gameProperties.gamePaused) {
gameProperties.wavePosition += currentWaveSpeed;
console.log('wavePosition', gameProperties.wavePosition, 'waveSpeed', currentWaveSpeed.toFixed(2));
} else {
// When paused, wave continues moving past the ship
gameProperties.wavePosition += currentWaveSpeed * 4; // Move 4x faster after catching player
}
// Update visual wave sprite position
if (this.waveSprite) {
this.waveSprite.setX(gameProperties.wavePosition + 350); // Offset for wave width
}
// Calculate distance between ship and RIGHT EDGE of wave (not center)
const waveWidth = 1098; // Wave sprite width
const waveRightEdge = gameProperties.wavePosition + (waveWidth / 2);
const distance = shipX - waveRightEdge;
// Clamp wave position to stay within reasonable bounds (only when not paused)
if (!gameProperties.gamePaused) {
if (distance > gameConfig.waveMaxDistance) {
gameProperties.wavePosition = shipX - gameConfig.waveMaxDistance - (waveWidth / 2);
} else if (distance < gameConfig.waveMinDistance) {
// Game over condition - wave caught the ship
this.handleWaveCaughtPlayer();
return;
}
}
// Calculate progress for UI (0 = safe, 1 = danger)
const maxSafeDistance = gameConfig.waveMaxDistance;
const progress = Math.max(0, Math.min(1, 1 - (distance / maxSafeDistance)));
// Update UI progress bar
if (this.GameUi && this.GameUi.updateWaveProgress) {
this.GameUi.updateWaveProgress(progress);
}
}
handleWaveCaughtPlayer() {
// Handle game over scenario when wave catches the player
console.log("Wave caught the player! Game Over!");
// Play game over sound
this.playGameOverSound();
// Calculate final score and send to FarcadeSDK (use same calculation as UI)
const finalScore = Math.max(0, Math.floor(gameProperties.score));
if (window.FarcadeSDK) {
window.FarcadeSDK.singlePlayer.actions.gameOver({
score: finalScore
});
console.log('Sent gameOver event to FarcadeSDK with score:', finalScore);
}
// Pause the game - stop terrain generation, ship movement, etc.
gameProperties.gamePaused = true;
gameProperties.shipCaught = true;
// Store ship's final position before freezing
if (this.birdSprite) {
gameProperties.shipFinalX = this.birdSprite.x;
gameProperties.shipFinalY = this.birdSprite.y;
gameProperties.shipFinalRotation = this.birdSprite.rotation;
}
// Completely stop ship physics
if (this.birdSprite && this.birdSprite.bodyId) {
const bodyId = this.birdSprite.bodyId;
b2Body_SetLinearVelocity(bodyId, { x: 0, y: 0 });
// Remove ship from physics world to prevent any movement
RemoveSpriteFromWorld(this.world.worldId, this.birdSprite);
}
// Hide the boat after 500ms to simulate being swept away
this.time.delayedCall(500, () => {
if (this.birdSprite) {
this.birdSprite.setVisible(false);
}
});
// Wave will continue moving past the ship in updateWaveChasing
}
update(time, delta) {
const worldId = this.world.worldId;
// Always update physics and wave chasing
WorldStep({ worldId, deltaTime: delta });
UpdateWorldSprites(worldId);
this.updateWaveChasing(time);
if (this.debug.visible) {
this.debug.clear();
b2World_Draw(worldId, this.worldDraw);
}
// Keep ship frozen at final position if caught by wave
if (gameProperties.shipCaught && this.birdSprite) {
this.birdSprite.setPosition(gameProperties.shipFinalX, gameProperties.shipFinalY);
this.birdSprite.setRotation(gameProperties.shipFinalRotation);
}
// Pause most game logic when wave has caught the ship
if (!gameProperties.gamePaused) {
this.birdDive();
this.limitVelocity();
this.updateShipRotation();
this.removeSegments();
this.cleanupCoins();
this.cleanupPowerUps();
this.checkCoinCollection();
this.checkPowerUpCollection();
this.updatePowerUpEffects();
this.updateParallaxBackground();
this.updateAirtimeTracking(time);
}
// Always update camera and score (even when paused)
this.updateCamera();
this.updateScore();
}
}
class GameUi extends Phaser.Scene {
constructor() {
super({ key: 'GameUi' });
}
preload() {
}
create() {
this.score_label_score = this.add.text(490, 32, '0', { font: '24px Fredoka', fill: '#ffffff' }).setOrigin(1, 0);
this.score_label = this.add.text(345, 30, 'Score: ', { font: '24px Fredoka-SemiBold', fill: '#ffffff' });
this.score_label.setX(this.score_label_score.x - this.score_label.width - this.score_label_score.width);
// Create wave progress bar UI
this.createWaveProgressBar();
// Hide UI elements initially until game starts
this.hideGameUI();
}
updateScore(score) {
this.score_label_score.text = score;
this.score_label.setX(this.score_label_score.x - this.score_label.width - this.score_label_score.width);
}
hideGameUI() {
// Hide score labels using alpha transparency
this.score_label.setAlpha(0);
this.score_label_score.setAlpha(0);
// Hide progress bar elements using alpha transparency
this.progressBarBg.setAlpha(0);
this.progressBarFill.setAlpha(0);
this.progressWave.setAlpha(0);
this.progressMask.setAlpha(0);
// Hide all progress fill images using alpha transparency
if (this.progressFillImages) {
this.progressFillImages.forEach(img => img.setAlpha(0));
}
}
showGameUI() {
// Fade in score labels smoothly
this.tweens.add({
targets: [this.score_label, this.score_label_score],
alpha: 1,
duration: 500,
ease: 'Power2.easeOut',
delay: 0 // Small delay after game start
});
// Fade in progress bar elements smoothly
this.tweens.add({
targets: [this.progressBarBg, this.progressBarFill, this.progressWave, this.progressMask],
alpha: 1,
duration: 500,
ease: 'Power2.easeOut',
delay: 0 // Slightly longer delay for progress bar
});
// Fade in all progress fill images smoothly
if (this.progressFillImages) {
this.tweens.add({
targets: this.progressFillImages,
alpha: 1,
duration: 500,
ease: 'Power2.easeOut',
delay: 0
});
}
}
createWaveProgressBar() {
// Progress bar dimensions and position
const barX = 15;
const barY = 30;
const barWidth = 300; // Will extend based on content
const barHeight = 28;
const barRadius = 14;
// Create progress bar background (transparent with white border)
this.progressBarBg = this.add.graphics();
this.progressBarBg.lineStyle(5, 0xffffff); // 5px white border
this.progressBarBg.strokeRoundedRect(barX, barY, barWidth, barHeight, barRadius);
this.progressBarBg.setDepth(10);
// Create container for repeating progress fill images
this.progressBarFill = this.add.container();
this.progressBarFill.setDepth(9);
// Pre-create all progress images to fill the entire bar
this.createProgressImages(barX, barY, barWidth, barHeight);
// Create ship icon at the end of progress bar (44px wide, maintaining aspect ratio)
// Ship is 187x166 at 1x, so for 44px width: height = (44/187) * 166 = 39px
this.progressShip = this.add.sprite(barX + barWidth, barY + barHeight / 2, 'ship', 0);
this.progressShip.setDisplaySize(44, 39);
this.progressShip.setDepth(11);
this.progressShip.setVisible(false);
// Create wave icon at the right end (50x50)
this.progressWave = this.add.image(barX + 32, barY + barHeight - 22, 'wave');
this.progressWave.setDisplaySize(49, 40);
this.progressWave.setDepth(11);
// Store initial positions for animation
this.progressBarData = {
barX,
barY,
barWidth,
barHeight,
barRadius,
shipStartX: barX + barWidth + 10,
waveX: barX + barWidth + 44 + 20
};
}
createProgressImages(barX, barY, barWidth, barHeight) {
const progressImageWidth = 43; // 86px @ 2x = 43px @ 1x
const progressImageHeight = 28; // 56px @ 2x = 28px @ 1x
const overlapOffset = -15; // Overlap images by 10px for seamless angled pattern
// Calculate how many images we need to cover the entire bar (with some extra for overlap)
const effectiveWidth = progressImageWidth + overlapOffset;
const numImages = Math.ceil((barWidth + Math.abs(overlapOffset)) / effectiveWidth) + 1;
this.progressFillImages = [];
// Create all progress images to fill the entire bar
for (let i = 0; i < numImages; i++) {
const progressImg = this.add.image(
barX - 10 + (i * effectiveWidth) + (progressImageWidth / 2),
barY + barHeight / 2,
'progress'
);
progressImg.setDisplaySize(progressImageWidth, progressImageHeight);
progressImg.setDepth(9);
this.progressFillImages.push(progressImg);
}
// Create mask graphics for revealing progress
this.progressMask = this.add.graphics();
this.progressMask.setDepth(8);
// Apply mask to all progress images
const mask = this.progressMask.createGeometryMask();
this.progressFillImages.forEach(img => img.setMask(mask));
// Set initial mask with small width (10px)
this.progressMask.fillStyle(0xffffff);
this.progressMask.fillRoundedRect(
barX + 2.5, // Account for border
barY + 2.5, // Account for border
10, // Initial small width
barHeight - 5, // Account for borders
Math.max(0, 14 - 2.5) // Slightly smaller radius to stay within border (barRadius = 14)
);
}
updateWaveProgress(progress) {
// progress is between 0 (safe) and 1 (danger)
const data = this.progressBarData;
// Calculate fill width based on progress
const fillWidth = data.barWidth * progress;
// Update wave position
this.progressWave.setX(Math.min(Math.max(data.barX + 32, fillWidth + 32), data.barX + data.barWidth - 10));
// Update mask to reveal only the filled portion
this.progressMask.clear();
if (progress > 0) {
// Create rounded rectangle mask that reveals the progress
this.progressMask.fillStyle(0xffffff);
this.progressMask.fillRoundedRect(
data.barX + 2.5, // Account for border
data.barY + 2.5, // Account for border
Math.max(fillWidth, 10),
data.barHeight - 5, // Account for borders
Math.max(0, data.barRadius - 2.5) // Slightly smaller radius to stay within border
);
} else {
this.progressMask.fillRoundedRect(
data.barX + 2.5, // Account for border
data.barY + 2.5, // Account for border
10,
data.barHeight - 5, // Account for borders
Math.max(0, data.barRadius - 2.5) // Slightly smaller radius to stay within border
);
}
}
}
const config = {
type: Phaser.AUTO,
width: 500, height: 800,
scene: [Example, GameUi],
fps: {
target: 60,
forceSetTimeOut: true
}
};
// Make game variable global so we can access it from startGame function
window.game = new Phaser.Game(config);
// FarcadeSDK event handlers
if (window.FarcadeSDK) {
window.FarcadeSDK.on("play_again", () => {
console.log('FarcadeSDK play_again event received');
const gameScene = window.game.scene.getScene('Game');
if (gameScene) {
gameScene.restartGame();
}
});
window.FarcadeSDK.on("toggle_mute", (data) => {
console.log('FarcadeSDK toggle_mute event received:', data.isMuted);
const gameScene = window.game.scene.getScene('Game');
if (gameScene) {
gameScene.setMuted(data.isMuted);
}
});
}
</script>
<script>
const screens = [...document.querySelectorAll('#howToPlay .screen')];
const counterEl = document.querySelector('.viewCount');
const nextBtn = document.querySelector('.nextBtn');
const prevBtn = document.querySelector('.prevBtn');
/* 0 → bottom card, last index → top card */
let current = screens.length - 1; // keep “howto‑1” on top
/* ---------- utils --------------------------------------------------- */
function refreshUI() {
counterEl.textContent = `${screens.length - current}/${screens.length}`;
/* button visibility is now reversed because we flipped the pile */
prevBtn.style.visibility = (current === screens.length - 1) ? 'hidden' : 'visible';
nextBtn.style.visibility = (current === 0) ? 'hidden' : 'visible';
}
/* put ‘stack’ on every card below the current one ------------------- */
function restack() {
screens.forEach((card, i) => {
card.classList.toggle('active', i === current);
card.classList.toggle('stack', i < current && !card.classList.contains('removed-right'));
});
}
/* ---------- navigation --------------------------------------------- */
function nextScreen() { // peel the top card away
if (current === 0) return; // already at the bottom
const out = screens[current];
out.classList.add('removed-right'); // fly off to the right
out.classList.remove('active', 'stack');
current -= 1; // reveal the one underneath
restack();
refreshUI();
}
function prevScreen() { // bring the last card back
if (current === screens.length - 1) return;
const comeback = screens[current + 1];
comeback.classList.remove('removed-right'); // slides back in
current += 1;
restack();
refreshUI();
}
/* ---------- initial state ------------------------------------------ */
screens.forEach((card, i) => {
if (i < current) card.classList.add('stack'); // only lower cards get “stack”
});
screens[current].classList.add('active'); // top card
refreshUI();
function howToPlay() {
document.getElementById('howToPlay').style.display = 'block';
document.getElementById('startScreen').style.display = 'none';
current = screens.length - 1;
screens.forEach(screen => screen.classList.remove('active', 'stack', 'removed-right'));
screens[screens.length - 1].classList.add('active');
screens[screens.length - 1].classList.remove('stack');
restack();
refreshUI();
}
function closeHowToPlay() {
document.getElementById('howToPlay').style.display = 'none';
document.getElementById('startScreen').style.display = 'block';
}
function startGame() {
const startScreen = document.getElementById('startScreen');
// Add fade-out class to trigger CSS transition
startScreen.classList.add('fade-out');
// After the fade transition completes, automatically launch the ship
setTimeout(() => {
// Get the main game scene (Example) and call fireBird to auto-launch
const exampleScene = window.game.scene.scenes[0];
if (exampleScene && exampleScene.fireBird) {
exampleScene.fireBird();
}
}, 500); // Match the CSS transition duration (0.8s)
}
</script>
</body>
</html>