403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/bc.the-word.com/openseal/noise.html
<!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>

Youez - 2016 - github.com/yon3zu
LinuXploit