3D Rabbit Game

Code Showcase with YouTube Video

Code & Video Showcase

Code Examples

HTML Code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />

    <!-- SEO Meta Tags -->
    <title>
      3D Rabbit Game - Free Online Browser Game | Collect Carrots & Jump
    </title>
    <meta
      name="description"
      content="Play the ultimate 3D Rabbit Game online! Control a cute rabbit, collect carrots, and jump around in this immersive browser-based game. Free to play, no download required!"
    />
    <meta
      name="keywords"
      content="3D rabbit game, online rabbit game, browser game, free games, carrot collecting game, jumping game, WebGL game, three.js game, animal games, cute rabbit, online games, 3D browser game, HTML5 game, mobile game"
    />
    <meta name="author" content="3D Rabbit Game Developer" />
    <meta name="robots" content="index, follow" />
    <meta name="language" content="English" />

    <!-- Open Graph Meta Tags -->
    <meta
      property="og:title"
      content="3D Rabbit Game - Free Online Browser Game"
    />
    <meta
      property="og:description"
      content="Play the ultimate 3D Rabbit Game! Control a cute rabbit, collect carrots, and jump around in this immersive browser-based game."
    />
    <meta property="og:type" content="website" />
    <meta
      property="og:url"
      content="https://oopsg.blogspot.com/2025/08/3d-rabbit-game.html"
    />
    <meta
      property="og:image"
      content="https://images.pexels.com/photos/326900/pexels-photo-326900.jpeg"
    />
    <meta property="og:site_name" content="3D Rabbit Game" />
    <meta property="og:locale" content="en_US" />

    <!-- Twitter Card Meta Tags -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta
      name="twitter:title"
      content="3D Rabbit Game - Free Online Browser Game"
    />
    <meta
      name="twitter:description"
      content="Play the ultimate 3D Rabbit Game! Control a cute rabbit, collect carrots, and jump around."
    />
    <meta
      name="twitter:image"
      content="https://images.pexels.com/photos/326900/pexels-photo-326900.jpeg"
    />

    <!-- Canonical URL -->
    <link
      rel="canonical"
      href="https://oopsg.blogspot.com/2025/08/3d-rabbit-game.html"
    />

    <!-- Favicon -->
    <link
      rel="icon"
      type="image/svg+xml"
      href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>ЁЯР░</text></svg>"
    />

    <!-- Additional SEO Tags -->
    <meta name="theme-color" content="#7beeff" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta
      name="apple-mobile-web-app-status-bar-style"
      content="black-translucent"
    />
    <meta name="apple-mobile-web-app-title" content="3D Rabbit Game" />

    <!-- Structured Data for SEO -->
    <script type="application/ld+json">
      {
        "@context": "https://schema.org",
        "@type": "VideoGame",
        "name": "3D Rabbit Game",
        "description": "An immersive 3D browser game where players control a cute rabbit to collect carrots and jump around in a 3D environment. Free to play online game built with WebGL and Three.js.",
        "url": "https://oopsg.blogspot.com/2025/08/3d-rabbit-game.html",
        "image": "https://images.pexels.com/photos/326900/pexels-photo-326900.jpeg",
        "genre": ["Action", "Arcade", "Family"],
        "gamePlatform": ["Web Browser", "Mobile", "Desktop"],
        "operatingSystem": ["Any"],
        "applicationCategory": "Game",
        "offers": {
          "@type": "Offer",
          "price": "0",
          "priceCurrency": "USD"
        },
        "aggregateRating": {
          "@type": "AggregateRating",
          "ratingValue": "4.8",
          "reviewCount": "127"
        },
        "author": {
          "@type": "Organization",
          "name": "3D Rabbit Game Studio"
        },
        "datePublished": "2025-01-21",
        "inLanguage": "en-US",
        "isAccessibleForFree": true,
        "keywords": "3D rabbit game, online games, browser games, WebGL games, carrot collecting, jumping games"
      }
    </script>

    <!-- Link to CSS file -->
    <link rel="stylesheet" href="style.css" />
  </head>
  
  <script
    type="text/javascript"
    src="//pl27304325.profitableratecpm.com/15/d9/76/15d97613d4fcce5aa926e43e870fe885.js"
  ></script>
  
  <body>
    <!-- SEO Hidden Content for Better Indexing -->
    <header style="position: absolute; left: -9999px">
      <h1>3D Rabbit Game - The Ultimate Browser Gaming Experience</h1>
      <p>
        Experience the most engaging 3D rabbit game online! Control your
        adorable rabbit character as you navigate through a stunning 3D
        environment, collecting delicious carrots and performing amazing jumps.
        This free browser-based game combines beautiful WebGL graphics with
        addictive gameplay.
      </p>

      <h2>Game Features</h2>
      <ul>
        <li>Stunning 3D graphics powered by WebGL and Three.js</li>
        <li>Intuitive mouse and touch controls for all devices</li>
        <li>Collect carrots to increase your score</li>
        <li>Perform spectacular rabbit jumps and flips</li>
        <li>Beautiful particle effects and animations</li>
        <li>Cross-platform compatibility - works on mobile and desktop</li>
        <li>No downloads required - play instantly in your browser</li>
        <li>High score system to track your best performances</li>
      </ul>

      <h3>How to Play the 3D Rabbit Game</h3>
      <p>
        Move your mouse or finger to control the rabbit's movement across the 3D
        environment. Click or tap to make the rabbit jump and perform
        spectacular flips. Collect the spinning carrots to earn points and beat
        your high score!
      </p>

      <h4>Game Controls</h4>
      <ul>
        <li>Mouse Movement / Touch: Control rabbit direction</li>
        <li>Click / Tap: Make the rabbit jump</li>
        <li>
          Objective: Collect as many carrots as possible before time runs out
        </li>
      </ul>
    </header>

    <main class="game-container ad-container">
      <h1 class="game-title">3D Rabbit Game</h1>

      <!-- Game UI: Score and Timer -->
      <div class="game-ui">
        <div class="ui-container">
          <div class="score-container">
            <div class="score-label">Score:</div>
            <div class="score-value" id="score">0</div>
          </div>
          <div class="timer-container">
            <div class="timer-label">Time:</div>
            <div class="timer-value" id="timer">2:00</div>
          </div>
        </div>
      </div>

      <canvas
        class="webgl"
        role="img"
        aria-label="3D Rabbit Game Canvas - Interactive game environment"
      ></canvas>
      <div id="instructions">- Tap/Click to jump -</div>
      <div id="credits">
        <p>Move the mouse/finger to control the rabbit | Collect the carrots</p>
      </div>
      <div class="audio-status" id="audioStatus">ЁЯФК Audio Ready</div>
    </main>

    <!-- ULTRA FIXED Game Over Modal -->
    <div
      class="modal-overlay"
      id="gameOverModal"
      role="dialog"
      aria-labelledby="gameOverTitle"
      aria-hidden="true"
    >
      <div class="modal-content">
        <h2 class="modal-title" id="gameOverTitle">Game Over!</h2>
        <div class="score-display">
          <div class="score-row">
            <label>Score:</label>
            <span id="finalScore">0</span>
          </div>
          <div class="score-row">
            <label>High Score:</label>
            <span id="highScore">0</span>
          </div>
        </div>
        <button
          class="play-again-btn"
          id="playAgainBtn"
          aria-label="Play the 3D rabbit game again"
          type="button"
        >
          Play Again
        </button>
      </div>
    </div>

    <!-- Audio Element -->
    <audio id="eatSound" preload="auto" style="display: none">
      <source
        src="https://res.cloudinary.com/tabrej/video/upload/v1754191326/Thumbnail%20Dot/Sound%20Effects/Eat_Sound_wdmult.mp3"
        type="audio/mpeg"
      />
    </audio>

    <!-- Shader Scripts -->
    <script type="x-shader/x-vertex" id="reflectorVertexShader">
      uniform mat4 textureMatrix;
      varying vec4 vUvReflection;
      varying vec2 vUv;

      #include <common>
      #include <shadowmap_pars_vertex>
      #include <logdepthbuf_pars_vertex>

      void main() {
          #include <beginnormal_vertex>
          #include <defaultnormal_vertex>
          #include <begin_vertex>

          vUvReflection = textureMatrix * vec4( position, 1.0 );
          vUv = uv;

          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

          #include <logdepthbuf_vertex>
          #include <worldpos_vertex>
          #include <shadowmap_vertex>
      }
    </script>

    <script type="x-shader/x-fragment" id="reflectorFragmentShader">
      uniform vec3 color;
      uniform sampler2D tDiffuse;
      uniform sampler2D tScratches;
      varying vec4 vUvReflection;
      varying vec2 vUv;

      #include <common>
      #include <packing>
      #include <lights_pars_begin>
      #include <shadowmap_pars_fragment>
      #include <shadowmask_pars_fragment>
      #include <logdepthbuf_pars_fragment>

      vec4 blur9(sampler2D image, vec4 uv, vec2 resolution, vec2 direction) {
          vec4 color = vec4(0.0);
          vec2 off1 = vec2(1.3846153846) * direction;
          vec2 off2 = vec2(3.2307692308) * direction;
          color += texture2DProj(image, uv) * 0.2270270270;
          color += texture2DProj(image, uv + vec4(off1 / resolution, off1 / resolution)) * 0.3162162162;
          color += texture2DProj(image, uv - vec4(off1 / resolution, off1 / resolution)) * 0.3162162162;
          color += texture2DProj(image, uv + vec4(off2 / resolution, off2 / resolution)) * 0.0702702703;
          color += texture2DProj(image, uv - vec4(off2 / resolution, off2 / resolution)) * 0.0702702703;
          return color;
      }

      float blendOverlay( float base, float blend ) {
          return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) );
      }

      vec3 blendOverlay( vec3 base, vec3 blend ) {
          return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) );
      }

      void main() {
          #include <logdepthbuf_fragment>

          vec4 displacement = vec4( sin(vUvReflection.y * 3.) * .05, sin(vUvReflection.x * 3.) * .05, 0.0, 0.0);
          vec2 resolution = vec2(30., 30.);
          vec4 base = blur9( tDiffuse, vUvReflection + displacement, resolution, vec2(1., 0.) ) * .25;
          base += blur9( tDiffuse, vUvReflection + displacement, resolution, vec2(-1., 0.) ) * .25;
          base += blur9( tDiffuse, vUvReflection + displacement, resolution, vec2(0, 1.) ) * .25;
          base += blur9( tDiffuse, vUvReflection + displacement, resolution, vec2(0, -1.) ) * .25;

          vec4 scratchesCol = texture2D( tScratches, vUv);

          vec3 col = mix(color, base.rgb, .5);
          col.rgb += scratchesCol.r * .02;
          col.gb -= scratchesCol.g * .01;
          col.gb -= (1.0 - getShadowMask() ) * .015;

          gl_FragColor = vec4(col, 1.0);
          #include <tonemapping_fragment>
          #include <colorspace_fragment>
      }
    </script>

    <script type="x-shader/x-vertex" id="simulationVertexShader">
      precision highp float;

      uniform float time;
      varying vec2 vUv;

      void main() {
          vUv = uv;
          vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
          gl_Position = projectionMatrix * modelViewPosition;
      }
    </script>

    <script type="x-shader/x-fragment" id="simulationFragmentShader">
      precision highp float;

      uniform sampler2D inputTexture;
      uniform vec2 blade1PosOld;
      uniform vec2 blade1PosNew;
      uniform float strength;
      uniform float time;
      varying vec2 vUv;

      float lineSegment(vec2 p, vec2 a, vec2 b, float thickness) {
          vec2 pa = p - a;
          vec2 ba = b - a;
          float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
          float idk = length(pa - ba*h);
          return smoothstep(thickness, .2 * thickness, idk);
      }

      void main(void) {
          vec4 prevTexture = texture2D(inputTexture, vUv);
          vec3 col = prevTexture.rgb * .999;
          if (strength>0.){
              float space = .001;
              float crease = .001;
              float thickness = .001 + strength * .001;
              float leftRed = lineSegment(vUv + space, blade1PosOld, blade1PosNew, thickness);
              float leftGreen = lineSegment(vUv + space + crease, blade1PosOld, blade1PosNew, thickness);
              float rightRed = lineSegment(vUv - space - crease, blade1PosOld, blade1PosNew, thickness);
              float rightGreen = lineSegment(vUv - space, blade1PosOld, blade1PosNew, thickness);
              col.r += ( leftRed + rightRed ) * strength * 3.0;
              col.g += ( leftGreen + rightGreen) * strength * 3.0;
              col.r = clamp(col.r, .0, 1.0);
              col.g = clamp(col.g, .0, 1.0);
          }
          gl_FragColor = vec4(col, 1.0);
      }
    </script>

    <script type="x-shader/x-fragment" id="outlineFragmentShader">
      uniform vec3 color;
      void main(void) {
          gl_FragColor = vec4( color, 1.0);
      }
    </script>

    <script type="x-shader/x-vertex" id="outlineVertexShader">
      uniform float size;
      uniform float time;

      void main() {
          vec3 transformed = position + normal * size * (1.0 + abs( sin ( position.y * time * .02 ) * 2.0 ));
          vec4 modelViewPosition = modelViewMatrix * vec4(transformed, 1.0);
          gl_Position = projectionMatrix * modelViewPosition;
      }
    </script>

    <!-- Link to JavaScript file -->
    <script type="module" src="script.js"></script>
  </body>
</html>

CSS Code

Copy HTML code to unlock CSS
@import url(https://fonts.googleapis.com/css?family=Open+Sans:600);

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: #1a1818;
  overflow: hidden;
  font-family: "Open Sans", sans-serif;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.game-container {
  width: 100vw;
  height: 100vh;
  position: relative;
  overflow: hidden;
  background: #332e2e;
}

.webgl {
  width: 100%;
  height: 100%;
  outline: none;
  cursor: crosshair;
  touch-action: none;
}

#credits {
  position: absolute;
  width: 100%;
  bottom: 20px;
  font-family: "Open Sans", sans-serif;
  color: #544027;
  font-size: 0.7em;
  text-transform: uppercase;
  text-align: center;
  z-index: 10;
  pointer-events: none;
}

#credits a {
  color: #7beeff;
  text-decoration: none;
}

#credits a:hover {
  color: #ff3434;
}

#instructions {
  position: absolute;
  width: 100%;
  bottom: 60px;
  font-family: "Open Sans", sans-serif;
  color: #ff3434;
  font-size: 0.7em;
  text-transform: uppercase;
  text-align: center;
  z-index: 10;
  pointer-events: none;
}

.game-title {
  position: absolute;
  top: 20px;
  width: 100%;
  text-align: center;
  font-family: "Open Sans", sans-serif;
  color: #7beeff;
  font-size: 1.5em;
  text-transform: uppercase;
  z-index: 10;
  pointer-events: none;
}

/* Game UI Elements */
.game-ui {
  position: absolute;
  top: 70px;
  left: 0;
  width: 100%;
  display: flex;
  justify-content: center;
  z-index: 10;
  pointer-events: none;
}

.ui-container {
  display: flex;
  gap: 20px;
  background-color: rgba(26, 24, 24, 0.7);
  padding: 10px 20px;
  border-radius: 20px;
  border: 1px solid #7beeff;
}

.score-container,
.timer-container {
  display: flex;
  align-items: center;
  gap: 10px;
}

.score-label,
.timer-label {
  color: #7beeff;
  font-size: 0.9em;
  text-transform: uppercase;
}

.score-value,
.timer-value {
  color: #ffffff;
  font-size: 1.2em;
  min-width: 40px;
  text-align: right;
}

/* Score Popup Animation */
.score-popup {
  position: absolute;
  color: #ff3434;
  font-size: 1.5em;
  font-weight: bold;
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  z-index: 15;
  opacity: 0;
  transform: translateY(0);
  animation: floatUp 1s ease-out forwards;
}

@keyframes floatUp {
  0% {
    opacity: 0;
    transform: translateY(0);
  }
  20% {
    opacity: 1;
  }
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translateY(-50px);
  }
}

/* COMPLETELY FIXED Game Over Modal for Mobile */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.95);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 999999;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
  touch-action: auto;
  -webkit-overflow-scrolling: touch;
}

.modal-overlay.active {
  opacity: 1;
  visibility: visible;
}

.modal-content {
  background-color: #332e2e;
  border-radius: 15px;
  padding: 40px 30px;
  width: 90%;
  max-width: 420px;
  text-align: center;
  border: 3px solid #7beeff;
  transform: scale(0.8);
  transition: transform 0.3s ease;
  box-shadow: 0 0 30px rgba(123, 238, 255, 0.5);
  pointer-events: auto;
  touch-action: auto;
  position: relative;
  max-height: 90vh;
  overflow-y: auto;
}

.modal-overlay.active .modal-content {
  transform: scale(1);
}

.modal-title {
  color: #7beeff;
  font-size: 2em;
  margin-bottom: 25px;
  text-transform: uppercase;
  font-weight: bold;
}

.score-display {
  margin-bottom: 30px;
}

.score-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
  font-size: 1.3em;
}

.score-row label {
  color: #ff3434;
  font-weight: bold;
}

.score-row span {
  color: white;
  font-weight: bold;
}

/* ULTRA FIXED Play Again Button - Mobile Optimized */
.play-again-btn {
  background: linear-gradient(145deg, #ff3434, #e62e2e);
  color: white;
  border: 3px solid #7beeff;
  padding: 25px 50px;
  font-size: 1.4em;
  border-radius: 15px;
  cursor: pointer;
  text-transform: uppercase;
  font-weight: bold;
  transition: all 0.2s ease;
  margin-top: 20px;
  min-height: 80px;
  min-width: 220px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(255, 52, 52, 0.3);
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  outline: none;
  position: relative;
  z-index: 1000002;
  display: block;
  margin-left: auto;
  margin-right: auto;
  font-family: "Open Sans", sans-serif;
  box-shadow: 0 8px 25px rgba(255, 52, 52, 0.4);
  transform: scale(1);
}

.play-again-btn:hover {
  background: linear-gradient(145deg, #e62e2e, #cc2626);
  transform: scale(1.05);
  box-shadow: 0 12px 30px rgba(255, 52, 52, 0.6);
  border-color: #ff3434;
}

.play-again-btn:active,
.play-again-btn.active {
  transform: scale(0.95);
  background: linear-gradient(145deg, #cc2626, #b32222);
  box-shadow: 0 4px 15px rgba(255, 52, 52, 0.5);
  border-color: #ffffff;
}

.play-again-btn:focus {
  outline: 3px solid #7beeff;
  outline-offset: 5px;
}

.score-bump {
  animation: scoreBump 0.5s ease;
}

@keyframes scoreBump {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.3);
  }
  100% {
    transform: scale(1);
  }
}

/* Audio Loading Indicator */
.audio-status {
  position: absolute;
  bottom: 100px;
  width: 100%;
  text-align: center;
  color: #7beeff;
  font-size: 0.7em;
  z-index: 10;
  pointer-events: none;
}

/* Responsive styles */
@media (max-width: 768px) {
  .game-title {
    font-size: 1.2em;
    top: 10px;
  }

  .game-ui {
    top: 50px;
  }

  .ui-container {
    gap: 10px;
    padding: 8px 15px;
  }

  .score-label,
  .timer-label {
    font-size: 0.8em;
  }

  .score-value,
  .timer-value {
    font-size: 1em;
  }

  #instructions {
    bottom: 40px;
    font-size: 0.6em;
  }

  #credits {
    bottom: 10px;
    font-size: 0.6em;
  }

  .modal-content {
    padding: 30px 25px;
    width: 95%;
    border-radius: 12px;
  }

  .modal-title {
    font-size: 1.6em;
  }

  .score-popup {
    font-size: 1.2em;
  }

  .audio-status {
    bottom: 80px;
    font-size: 0.6em;
  }

  .play-again-btn {
    padding: 22px 45px;
    font-size: 1.3em;
    min-height: 75px;
    min-width: 200px;
    border-width: 2px;
  }
}

@media (max-width: 480px) {
  .game-title {
    font-size: 1em;
  }

  .ui-container {
    padding: 5px 10px;
  }

  .modal-content {
    padding: 25px 20px;
    width: 98%;
  }

  .play-again-btn {
    padding: 20px 40px;
    font-size: 1.2em;
    min-height: 70px;
    min-width: 180px;
  }

  .score-row {
    font-size: 1.1em;
  }

  .modal-title {
    font-size: 1.4em;
  }
}

/* Additional mobile fixes */
@media (max-height: 600px) {
  .modal-content {
    padding: 20px;
    max-height: 95vh;
  }

  .modal-title {
    font-size: 1.3em;
    margin-bottom: 15px;
  }

  .score-display {
    margin-bottom: 20px;
  }

  .play-again-btn {
    padding: 18px 35px;
    font-size: 1.1em;
    min-height: 65px;
  }
}

/* Ads Friendly Styles */
.ad-container {
  position: relative;
  z-index: 1;
}

/* Ensure game doesn't interfere with ad overlays */
.game-container canvas {
  z-index: 1;
}

/* High z-index for modal to appear above everything */
.modal-overlay {
  z-index: 999999 !important;
}

JavaScript Code

Copy CSS code to unlock JavaScript
import * as THREE from "https://esm.sh/three@0.156.1";
import { OrbitControls } from "https://esm.sh/three@0.156.1/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "https://esm.sh/three@0.156.1/examples/jsm/loaders/GLTFLoader";
import { Reflector } from "https://esm.sh/three@0.156.1/examples/jsm/objects/Reflector";
import gsap from "https://esm.sh/gsap";

class App {
  constructor() {
    this.winWidth = window.innerWidth;
    this.winHeight = window.innerHeight;
    this.isMobile = window.innerWidth < 768;
    this.gltfFile = "https://assets.codepen.io/264161/rabbit6.glb";
    this.isReady = false;

    // Game state variables
    this.score = 0;
    this.highScore = this.getHighScore();
    this.gameActive = false;
    this.gameTime = 120; // 2 minutes in seconds
    this.pointsPerCarrot = 5; // Points per carrot
    this.modalOpen = false; // Track modal state
    this.isRestarting = false; // Prevent multiple restart calls

    // Audio setup
    this.setupAudio();

    this.setupLoadingIndicator();
    this.setupGameOverModal();
    this.loadAssets();
  }

  setupAudio() {
    this.eatSound = document.getElementById("eatSound");
    this.audioStatusEl = document.getElementById("audioStatus");
    this.audioInitialized = false;

    // Set volume
    this.eatSound.volume = 0.7;

    // Initialize audio on first user interaction
    const initAudio = async (event) => {
      if (!this.audioInitialized) {
        try {
          // Create a new Audio instance for better mobile compatibility
          this.audioContext = new (window.AudioContext ||
            window.webkitAudioContext)();

          // Test audio playability
          const testPlay = this.eatSound.play();
          if (testPlay) {
            await testPlay;
            this.eatSound.pause();
            this.eatSound.currentTime = 0;
          }

          this.audioInitialized = true;
          this.audioStatusEl.textContent = "ЁЯФК Audio Ready";
          this.audioStatusEl.style.color = "#7beeff";

          // Remove listeners after successful initialization
          document.removeEventListener("click", initAudio);
          document.removeEventListener("touchstart", initAudio);
          document.removeEventListener("touchend", initAudio);
          document.removeEventListener("keydown", initAudio);
        } catch (error) {
          console.log("Audio initialization failed:", error);
          this.audioStatusEl.textContent = "ЁЯФЗ Audio Unavailable";
          this.audioStatusEl.style.color = "#ff3434";
        }
      }
    };

    // Add multiple event listeners for audio initialization
    document.addEventListener("click", initAudio);
    document.addEventListener("touchstart", initAudio);
    document.addEventListener("touchend", initAudio);
    document.addEventListener("keydown", initAudio);

    // Handle audio loading errors
    this.eatSound.addEventListener("error", () => {
      this.audioStatusEl.textContent = "ЁЯФЗ Audio Unavailable";
      this.audioStatusEl.style.color = "#ff3434";
    });
  }

  playEatSound() {
    if (this.audioInitialized && this.eatSound) {
      try {
        // Reset the audio to beginning and play
        this.eatSound.currentTime = 0;
        const playPromise = this.eatSound.play();

        if (playPromise !== undefined) {
          playPromise.catch((error) => {
            console.log("Audio play failed:", error);
          });
        }
      } catch (error) {
        console.log("Audio error:", error);
      }
    }
  }

  setupLoadingIndicator() {
    // Create a simple loading indicator
    const loadingDiv = document.createElement("div");
    loadingDiv.id = "loading";
    loadingDiv.style.position = "absolute";
    loadingDiv.style.top = "50%";
    loadingDiv.style.left = "50%";
    loadingDiv.style.transform = "translate(-50%, -50%)";
    loadingDiv.style.color = "#7beeff";
    loadingDiv.style.fontSize = "1.2em";
    loadingDiv.style.fontFamily = '"Open Sans", sans-serif';
    loadingDiv.textContent = "Loading 3D Rabbit Game...";
    document.querySelector(".game-container").appendChild(loadingDiv);
    this.loadingIndicator = loadingDiv;
  }

  // COMPLETELY REWRITTEN AND SIMPLIFIED modal setup for guaranteed mobile compatibility
  setupGameOverModal() {
    // Get modal elements
    this.modal = document.getElementById("gameOverModal");
    this.finalScoreEl = document.getElementById("finalScore");
    this.highScoreEl = document.getElementById("highScore");
    this.playAgainBtn = document.getElementById("playAgainBtn");

    // ULTRA SIMPLE, RELIABLE restart handler
    const handleRestart = () => {
      console.log("Play Again button clicked!");

      // Immediate visual feedback
      this.playAgainBtn.classList.add("active");

      // Prevent multiple calls
      if (this.isRestarting) {
        console.log("Already restarting, ignoring click");
        return;
      }

      this.isRestarting = true;

      // Hide modal immediately
      this.hideModal();

      // Restart game after brief delay
      setTimeout(() => {
        this.resetGameState();
        this.startGame();
        this.isRestarting = false;
        this.playAgainBtn.classList.remove("active");
      }, 300);
    };

    // REMOVE ALL existing event listeners and add ONE reliable handler
    this.playAgainBtn.onclick = null;
    this.playAgainBtn.ontouchstart = null;
    this.playAgainBtn.ontouchend = null;

    // Clone button to remove all event listeners
    const newBtn = this.playAgainBtn.cloneNode(true);
    this.playAgainBtn.parentNode.replaceChild(newBtn, this.playAgainBtn);
    this.playAgainBtn = newBtn;

    // Add SINGLE, simple event listener that works on all devices
    this.playAgainBtn.addEventListener("click", handleRestart, {
      passive: false,
      once: false,
    });

    // Additional mobile touch support
    this.playAgainBtn.addEventListener(
      "touchend",
      (e) => {
        e.preventDefault();
        e.stopPropagation();
        handleRestart();
      },
      {
        passive: false,
        once: false,
      }
    );

    // Prevent modal background clicks
    this.modal.addEventListener("click", (e) => {
      if (e.target === this.modal) {
        e.preventDefault();
        e.stopPropagation();
      }
    });
  }

  hideModal() {
    this.modal.classList.remove("active");
    this.modal.setAttribute("aria-hidden", "true");
    this.modalOpen = false;

    // Re-enable game canvas interactions
    const canvas = document.querySelector(".webgl");
    if (canvas) {
      canvas.style.pointerEvents = "auto";
    }
  }

  showModal() {
    this.modalOpen = true;

    // Disable game canvas interactions
    const canvas = document.querySelector(".webgl");
    if (canvas) {
      canvas.style.pointerEvents = "none";
    }

    // Update modal content
    this.finalScoreEl.textContent = this.score;
    this.highScoreEl.textContent = this.highScore;

    // Show modal
    this.modal.classList.add("active");
    this.modal.setAttribute("aria-hidden", "false");

    // Focus button after modal is visible
    setTimeout(() => {
      if (this.playAgainBtn) {
        this.playAgainBtn.focus();
      }
    }, 400);
  }

  resetGameState() {
    // Clear any running timers
    if (this.timerInterval) {
      clearInterval(this.timerInterval);
      this.timerInterval = null;
    }

    // Reset all game variables
    this.score = 0;
    this.gameTime = 120;
    this.gameActive = false;
    this.isExploding = false;
    this.isJumping = false;
    this.isLanding = false;

    // Reset UI
    document.getElementById("score").textContent = "0";
    document.getElementById("timer").textContent = "2:00";

    // Reset rabbit position and rotation
    if (this.rabbit) {
      this.rabbit.position.set(0, 0, 0);
      this.rabbit.rotation.set(0, 0, 0);
      if (this.rabbitBody) {
        this.rabbitBody.rotation.set(0, 0, 0);
      }
    }

    // Hide carrot
    if (this.carrot) {
      this.carrot.visible = false;
    }

    // Reset hero parameters
    this.heroSpeed = new THREE.Vector2(0, 0);
    this.heroAcc = new THREE.Vector2(0, 0);
    this.targetHeroUVPos = new THREE.Vector2(0.5, 0.5);
    this.heroOldUVPos = new THREE.Vector2(0.5, 0.5);
    this.heroNewUVPos = new THREE.Vector2(0.5, 0.5);

    // Hide all particles
    if (this.particles1) {
      this.particles1.forEach((p) => p.scale.set(0, 0, 0));
    }
    if (this.particles2) {
      this.particles2.forEach((p) => p.scale.set(0, 0, 0));
    }

    console.log("Game state reset complete");
  }

  getHighScore() {
    return parseInt(localStorage.getItem("rabbitGameHighScore") || 0);
  }

  saveHighScore(score) {
    localStorage.setItem("rabbitGameHighScore", score);
  }

  updateScore(points = null) {
    const pointsToAdd = points !== null ? points : this.pointsPerCarrot;
    this.score += pointsToAdd;
    const scoreElement = document.getElementById("score");
    scoreElement.textContent = this.score;

    // Add animation effect
    scoreElement.classList.remove("score-bump");
    // Trigger reflow to restart the animation
    void scoreElement.offsetWidth;
    scoreElement.classList.add("score-bump");
  }

  showScorePopup(position) {
    // Create a new score popup element
    const popup = document.createElement("div");
    popup.className = "score-popup";
    popup.textContent = `+${this.pointsPerCarrot}`;

    // Calculate screen position from 3D world position
    const widthHalf = this.winWidth / 2;
    const heightHalf = this.winHeight / 2;

    // Clone the position to avoid modifying the original
    const pos = position.clone();

    // Project the 3D position to screen space
    pos.project(this.camera);

    // Convert to screen coordinates
    const x = pos.x * widthHalf + widthHalf;
    const y = -(pos.y * heightHalf) + heightHalf;

    // Set position
    popup.style.left = `${x}px`;
    popup.style.top = `${y}px`;

    // Add to the DOM
    document.querySelector(".game-container").appendChild(popup);

    // Remove after animation completes
    setTimeout(() => {
      popup.remove();
    }, 1000);
  }

  updateTimer() {
    if (!this.gameActive) return;

    this.gameTime--;

    const minutes = Math.floor(this.gameTime / 60);
    const seconds = this.gameTime % 60;

    // Format timer display
    document.getElementById("timer").textContent = `${minutes}:${
      seconds < 10 ? "0" : ""
    }${seconds}`;

    // Check if time is up
    if (this.gameTime <= 0) {
      this.endGame();
    }
  }

  startGame() {
    console.log("Starting new game...");

    this.gameActive = true;

    // Start timer
    this.timerInterval = setInterval(() => this.updateTimer(), 1000);

    // Spawn initial carrot after a brief delay
    setTimeout(() => {
      if (this.gameActive) {
        this.spawnCarrot();
      }
    }, 1000);

    console.log("Game started successfully");
  }

  endGame() {
    console.log("Ending game...");

    this.gameActive = false;

    // Clear timer
    if (this.timerInterval) {
      clearInterval(this.timerInterval);
      this.timerInterval = null;
    }

    // Update high score if needed
    if (this.score > this.highScore) {
      this.highScore = this.score;
      this.saveHighScore(this.highScore);
    }

    // Show modal after slight delay
    setTimeout(() => {
      this.showModal();
    }, 500);
  }

  loadAssets() {
    const loaderModel = new GLTFLoader();
    loaderModel.load(
      this.gltfFile,
      (gltf) => {
        this.model = gltf.scene;
        this.setUpScene();
        // Remove loading indicator when ready
        if (this.loadingIndicator) {
          this.loadingIndicator.remove();
        }
        this.isReady = true;

        // Start the game after assets are loaded
        this.startGame();
      },
      // Progress callback
      (xhr) => {
        if (this.loadingIndicator) {
          const percent = Math.floor((xhr.loaded / xhr.total) * 100);
          this.loadingIndicator.textContent = `Loading 3D Rabbit Game... ${percent}%`;
        }
      },
      // Error callback
      (error) => {
        console.error("Error loading model:", error);
        if (this.loadingIndicator) {
          this.loadingIndicator.textContent =
            "Error loading game assets. Please refresh the page.";
          this.loadingIndicator.style.color = "#ff3434";
        }
      }
    );
  }

  setUpScene() {
    // Scene
    this.scene = new THREE.Scene();
    this.bgrColor = 0x332e2e;
    this.fog = new THREE.Fog(this.bgrColor, 13, 20);
    this.scene.fog = this.fog;

    // Camera setup based on device
    const fov = this.isMobile ? 70 : 60;
    this.camera = new THREE.PerspectiveCamera(
      fov,
      this.winWidth / this.winHeight,
      1,
      100
    );

    // Adjust camera position based on device
    if (this.isMobile) {
      this.camera.position.set(0, 5, 10);
    } else {
      this.camera.position.set(0, 4, 8);
    }

    this.camera.lookAt(new THREE.Vector3());
    this.scene.add(this.camera);

    // Hero params
    this.heroAngularSpeed = 0;
    this.heroOldRot = 0;
    this.heroDistance = 0;
    this.heroOldUVPos = new THREE.Vector2(0.5, 0.5);
    this.heroNewUVPos = new THREE.Vector2(0.5, 0.5);
    this.heroSpeed = new THREE.Vector2(0, 0);
    this.heroAcc = new THREE.Vector2(0, 0);
    this.targetHeroUVPos = new THREE.Vector2(0.5, 0.5);
    this.targetHeroAbsMousePos = new THREE.Vector2(0, 0);
    this.raycaster = new THREE.Raycaster();
    this.mouse = new THREE.Vector2();
    this.isJumping = this.isLanding = false;
    this.jumpParams = { jumpProgress: 0, landProgress: 0 };

    // Clock
    this.clock = new THREE.Clock();
    this.time = 0;
    this.deltaTime = 0;

    // Core
    this.createRenderer();
    this.createSim();
    this.createListeners();

    // Environment
    this.floorSize = this.determineFloorSize();
    this.createMaterials();
    this.processModel();
    this.createFloor();
    this.createLine();
    this.createLight();
    this.createParticles();

    // Render loop
    this.draw();
  }

  // Determine floor size based on device
  determineFloorSize() {
    return this.isMobile ? 20 : 30;
  }

  processModel() {
    this.rabbit = this.model.getObjectByName("Rabbit");
    this.rabbitBody = this.model.getObjectByName("body");
    this.earRight = this.model.getObjectByName("earRight");
    this.earLeft = this.model.getObjectByName("earLeft");
    this.tail = this.model.getObjectByName("tail");
    this.footLeft = this.model.getObjectByName("footLeft");
    this.footRight = this.model.getObjectByName("footRight");
    this.eyeLeft = this.model.getObjectByName("eyeLeft");
    this.eyeRight = this.model.getObjectByName("eyeRight");
    this.carrot = this.model.getObjectByName("carrot");
    this.carrotLeaf = this.model.getObjectByName("carrotLeaf");
    this.carrotLeaf2 = this.model.getObjectByName("carrotLeaf2");
    this.carrot.rotation.z = 0.2;
    this.carrot.rotation.x = 0.2;
    this.rabbitBody.material = this.primMat;
    this.earRight.material = this.primMat;
    this.earLeft.material = this.primMat;
    this.tail.material = this.primMat;
    this.footLeft.material = this.secMat;
    this.footRight.material = this.secMat;
    this.eyeLeft.material = this.secMat;
    this.eyeRight.material = this.secMat;
    this.carrot.material = this.bonusMat;
    this.carrotLeaf.material = this.primMat;
    this.carrotLeaf2.material = this.primMat;

    this.addOutline(this.rabbitBody);
    this.addOutline(this.earRight);
    this.addOutline(this.earLeft);
    this.addOutline(this.tail);
    this.addOutline(this.carrot);

    this.rabbit.traverse((object) => {
      if (object.isMesh) {
        object.castShadow = true;
        object.receiveShadow = true;
      }
    });

    this.carrot.traverse((object) => {
      if (object.isMesh) {
        object.castShadow = true;
      }
    });

    this.scene.add(this.rabbit);
    this.scene.add(this.carrot);

    // Hide carrot initially until spawned
    this.carrot.visible = false;
  }

  createFloor() {
    this.floor = new Reflector(
      new THREE.PlaneGeometry(this.floorSize, this.floorSize),
      {
        color: new THREE.Color(this.bgrColor),
        textureWidth: 1024,
        textureHeight: 1024,
      }
    );
    this.floor.rotation.x = -Math.PI / 2;
    this.floor.receiveShadow = true;
    this.modifyFloorShader();

    this.scene.add(this.floor);
  }

  createLine() {
    const material = new THREE.LineDashedMaterial({
      color: 0x7beeff,
      linewidth: 1,
      scale: 1,
      dashSize: 0.2,
      gapSize: 0.1,
    });

    const points = [];
    points.push(new THREE.Vector3(0, 0.2, 0));
    points.push(new THREE.Vector3(3, 0.2, 3));
    const geometry = new THREE.BufferGeometry().setFromPoints(points);
    this.line = new THREE.Line(geometry, material);
    this.scene.add(this.line);
  }

  createParticles() {
    let bodyCount = this.isMobile ? 15 : 20;
    let tailCount = this.isMobile ? 3 : 5;
    let particleGeom = new THREE.BoxGeometry(0.2, 0.2, 0.2, 1, 1, 1);
    this.particles1 = [];
    this.particles2 = [];

    let i = 0;

    for (i = 0; i < bodyCount; i++) {
      let m = new THREE.Mesh(particleGeom, this.bonusMat);
      this.particles1.push(m);
      m.scale.set(0, 0, 0);
      this.scene.add(m);
    }

    for (i = 0; i < tailCount; i++) {
      let m = new THREE.Mesh(particleGeom, this.primMat);
      this.particles2.push(m);
      m.scale.set(0, 0, 0);
      this.scene.add(m);
    }
  }

  createLight() {
    this.ambientLight = new THREE.AmbientLight(0xffffff);
    this.scene.add(this.ambientLight);

    this.light = new THREE.DirectionalLight(0xffffff, 1);
    this.light.position.set(1, 5, 1);
    this.light.castShadow = true;
    this.light.shadow.mapSize.width = 512;
    this.light.shadow.mapSize.height = 512;
    this.light.shadow.camera.near = 0.5;
    this.light.shadow.camera.far = 12;
    this.light.shadow.camera.left = -12;
    this.light.shadow.camera.right = 12;
    this.light.shadow.camera.bottom = -12;
    this.light.shadow.camera.top = 12;
    this.light.shadow.radius = 3;
    this.light.shadow.blurSamples = 4;
    this.scene.add(this.light);
  }

  createRenderer() {
    const canvas = document.querySelector("canvas.webgl");
    this.renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      preserveDrawingBuffer: true,
    });
    this.renderer.setClearColor(new THREE.Color(this.bgrColor));

    this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
    this.renderer.setSize(this.winWidth, this.winHeight);
    this.renderer.toneMapping = THREE.LinearToneMapping;
    this.renderer.toneMappingExposure = 1;
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.VSMShadowMap;
    this.renderer.localClippingEnabled = true;
  }

  createSim() {
    const fragmentShader = document.getElementById(
      "simulationFragmentShader"
    ).textContent;
    const vertexShader = document.getElementById(
      "simulationVertexShader"
    ).textContent;

    this.floorSimMat = new THREE.ShaderMaterial({
      uniforms: {
        inputTexture: { type: "t", value: null },
        time: { value: 0.0 },
        blade1PosOld: { value: new THREE.Vector2(0.5, 0.5) },
        blade1PosNew: { value: new THREE.Vector2(0.5, 0.5) },
        strength: { value: 0.0 },
      },
      vertexShader,
      fragmentShader,
    });
    this.bufferSim = new BufferSim(
      this.renderer,
      1024,
      1024,
      this.floorSimMat
    );
  }

  createMaterials() {
    // Materials
    this.primMat = new THREE.MeshToonMaterial({ color: 0x7beeff });
    this.secMat = new THREE.MeshToonMaterial({ color: this.bgrColor });
    this.bonusMat = new THREE.MeshToonMaterial({ color: 0xff3434 });

    // outline Material
    const fragmentShader = document.getElementById(
      "outlineFragmentShader"
    ).textContent;
    const vertexShader = document.getElementById(
      "outlineVertexShader"
    ).textContent;
    this.outlineMat = new THREE.ShaderMaterial({
      uniforms: {
        color: { value: new THREE.Color(0x000000) },
        size: { type: "f", value: 0.02 },
      },
      vertexShader,
      fragmentShader,
      side: THREE.BackSide,
    });
  }

  addOutline(origin) {
    let outline = origin.clone();
    outline.children = [];
    outline.position.set(0, 0, 0);
    outline.rotation.x = 0;
    outline.rotation.y = 0;
    outline.rotation.z = 0;
    outline.scale.set(1, 1, 1);
    outline.material = this.outlineMat;
    origin.add(outline);
    return outline;
  }

  // SIMPLIFIED event listener management with proper modal checking
  createListeners() {
    window.addEventListener("resize", this.onWindowResize.bind(this));

    if ("ontouchstart" in window || navigator.maxTouchPoints) {
      // Touch events for mobile with improved modal checking
      document.addEventListener(
        "touchmove",
        this.onTouchMove.bind(this),
        { passive: false }
      );
      document.addEventListener(
        "touchstart",
        this.onTouchStart.bind(this),
        { passive: false }
      );
    } else {
      // Mouse events for desktop
      document.addEventListener(
        "mousemove",
        this.onMouseMove.bind(this),
        false
      );
      document.addEventListener(
        "mousedown",
        this.onMouseDown.bind(this),
        false
      );
    }
  }

  draw() {
    this.updateGame();
    this.renderer.render(this.scene, this.camera);
    window.requestAnimationFrame(this.draw.bind(this));
  }

  updateGame() {
    if (!this.isReady) return;

    this.dt = Math.min(this.clock.getDelta(), 0.3);
    this.time += this.dt;

    if (this.rabbit && this.line) {
      // Elastic string simulation
      let constrainUVPosX = this.constrain(
        this.targetHeroUVPos.x - 0.5,
        -0.3,
        0.3
      );
      let constrainUVPosY = this.constrain(
        this.targetHeroUVPos.y - 0.5,
        -0.3,
        0.3
      );
      this.targetHeroAbsMousePos.x = constrainUVPosX * this.floorSize;
      this.targetHeroAbsMousePos.y = -constrainUVPosY * this.floorSize;

      let dx = this.targetHeroAbsMousePos.x - this.rabbit.position.x;
      let dy = this.targetHeroAbsMousePos.y - this.rabbit.position.z;

      let angle = Math.atan2(dy, dx);
      this.heroDistance = Math.sqrt(dx * dx + dy * dy);
      let ax = dx * this.dt * 0.5;
      let ay = dy * this.dt * 0.5;

      this.heroSpeed.x += ax;
      this.heroSpeed.y += ay;

      this.heroSpeed.x *= Math.pow(this.dt, 0.005);
      this.heroSpeed.y *= Math.pow(this.dt, 0.005);

      this.rabbit.position.x += this.heroSpeed.x;
      this.rabbit.position.z += this.heroSpeed.y;
      let targetRot = -angle + Math.PI / 2;

      if (this.heroDistance > 0.3)
        this.rabbit.rotation.y +=
          this.getShortestAngle(targetRot - this.rabbit.rotation.y) *
          3 *
          this.dt;
      this.heroAngularSpeed = this.getShortestAngle(
        this.rabbit.rotation.y - this.heroOldRot
      );

      this.heroOldRot = this.rabbit.rotation.y;
      if (!this.isJumping) {
        this.earLeft.rotation.x = this.earRight.rotation.x =
          -this.heroSpeed.length() * 2;
      }

      let p = this.line.geometry.attributes.position.array;
      p[0] = this.targetHeroAbsMousePos.x;
      p[2] = this.targetHeroAbsMousePos.y;
      p[3] = this.rabbit.position.x;
      p[4] = this.rabbit.position.y;
      p[5] = this.rabbit.position.z;

      this.line.geometry.attributes.position.needsUpdate = true;
      this.line.computeLineDistances();

      this.heroNewUVPos = new THREE.Vector2(
        0.5 + this.rabbit.position.x / this.floorSize,
        0.5 - this.rabbit.position.z / this.floorSize
      );

      this.floorSimMat.uniforms.time.value += this.dt;
      this.floorSimMat.uniforms.blade1PosNew.value = this.heroNewUVPos;
      this.floorSimMat.uniforms.blade1PosOld.value = this.heroOldUVPos;
      this.floorSimMat.uniforms.strength.value = this.isJumping
        ? 0
        : 1 / (1 + this.heroSpeed.length() * 10);
      this.bufferSim.render();
      this.renderer.setRenderTarget(null);

      this.floor.material.uniforms.tScratches.value =
        this.bufferSim.output.texture;

      this.heroOldUVPos = this.heroNewUVPos.clone();
      this.carrot.rotation.y += this.dt;

      this.testCollision();
    }
  }

  onWindowResize() {
    this.winWidth = window.innerWidth;
    this.winHeight = window.innerHeight;
    this.isMobile = window.innerWidth < 768;

    // Update camera aspect and position based on device
    this.camera.aspect = this.winWidth / this.winHeight;
    if (this.isMobile && this.camera) {
      this.camera.fov = 70;
      this.camera.position.set(0, 5, 10);
    } else if (this.camera) {
      this.camera.fov = 60;
      this.camera.position.set(0, 4, 8);
    }

    this.camera.updateProjectionMatrix();
    this.renderer.setSize(this.winWidth, this.winHeight);

    // Update floor size based on device
    if (this.floor) {
      const newFloorSize = this.determineFloorSize();
      if (newFloorSize !== this.floorSize) {
        this.floorSize = newFloorSize;
        // Re-create floor with new size
        this.scene.remove(this.floor);
        this.createFloor();
      }
    }
  }

  onMouseMove(event) {
    // Don't process mouse events when modal is open
    if (this.modalOpen) return;

    const x = (event.clientX / this.winWidth) * 2 - 1;
    const y = -((event.clientY / this.winHeight) * 2 - 1);
    this.updateMouse(x, y);
  }

  onTouchMove(event) {
    // Don't process touch events when modal is open
    if (this.modalOpen) return;

    if (event.touches.length == 1) {
      event.preventDefault();
      const x = (event.touches[0].pageX / this.winWidth) * 2 - 1;
      const y = -((event.touches[0].pageY / this.winHeight) * 2 - 1);
      this.updateMouse(x, y);
    }
  }

  onTouchStart(event) {
    // Don't process touch events when modal is open
    if (this.modalOpen) return;

    // Handle touch start for jumping
    if (this.rabbit && !this.isJumping && this.gameActive) {
      this.jump();
    }

    // Also update position on touch start for immediate movement
    if (event.touches.length == 1) {
      event.preventDefault();
      const x = (event.touches[0].pageX / this.winWidth) * 2 - 1;
      const y = -((event.touches[0].pageY / this.winHeight) * 2 - 1);
      this.updateMouse(x, y);
    }
  }

  updateMouse(x, y) {
    this.mouse.x = x;
    this.mouse.y = y;
    if (this.floor) this.raycast();
  }

  onMouseDown(event) {
    // Don't process mouse events when modal is open
    if (this.modalOpen) return;

    if (this.rabbit && !this.isJumping && this.gameActive) this.jump();
  }

  jump() {
    this.isJumping = true;
    let turns = Math.floor(this.heroSpeed.length() * 5) + 1;
    let jumpDuration = 0.5 + turns * 0.2;
    let targetRot =
      this.heroAngularSpeed > 0
        ? Math.PI * 2 * turns
        : -Math.PI * 2 * turns;

    gsap.to(this.rabbitBody.rotation, {
      duration: jumpDuration,
      ease: "linear.none",
      y: targetRot,
      onComplete: () => {
        this.rabbitBody.rotation.y = 0;
      },
    });

    gsap.to([this.earLeft.rotation, this.earRight.rotation], {
      duration: jumpDuration * 0.8,
      ease: "power4.out",
      x: Math.PI / 4,
    });
    gsap.to([this.earLeft.rotation, this.earRight.rotation], {
      duration: jumpDuration * 0.2,
      delay: jumpDuration * 0.8,
      ease: "power4.in",
      x: 0,
    });

    gsap.to(this.jumpParams, {
      duration: jumpDuration * 0.5,
      ease: "power2.out",
      jumpProgress: 0.5,
      onUpdate: () => {
        let sin = Math.sin(this.jumpParams.jumpProgress * Math.PI);
        this.rabbit.position.y = Math.pow(sin, 4) * turns;
      },
    });
    gsap.to(this.jumpParams, {
      duration: jumpDuration * 0.5,
      ease: "power2.in",
      delay: jumpDuration * 0.5,
      jumpProgress: 1,
      onUpdate: () => {
        let sin = Math.sin(this.jumpParams.jumpProgress * Math.PI);
        this.rabbit.position.y = Math.pow(sin, 1) * turns;
      },
      onComplete: () => {
        this.rabbit.position.y = 0;
        this.jumpParams.jumpProgress = 0;
        this.isJumping = false;
      },
    });
  }

  raycast() {
    // Direct raycast from mouse to floor
    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects([this.floor]);

    if (intersects.length > 0) {
      // Get the exact point where the ray intersects the floor
      this.targetHeroUVPos.x = intersects[0].uv.x;
      this.targetHeroUVPos.y = intersects[0].uv.y;
    }
  }

  getShortestAngle(v) {
    let a = v % (Math.PI * 2);
    if (a < -Math.PI) a += Math.PI * 2;
    else if (a > Math.PI) a -= Math.PI * 2;
    return a;
  }

  constrain(v, vMin, vMax) {
    return Math.min(vMax, Math.max(vMin, v));
  }

  testCollision() {
    if (this.isExploding || !this.gameActive) return;

    if (!this.carrot.visible) return;

    let distVec = this.rabbit.position.clone();
    distVec.sub(this.carrot.position);
    let l = distVec.length();
    if (l <= 1) {
      // Store carrot position before hiding it
      const carrotPos = this.carrot.position.clone();
      this.carrot.visible = false;

      // Play eating sound automatically when carrot is collected
      this.playEatSound();

      // Show +5 score popup at carrot position
      this.showScorePopup(carrotPos);

      this.explode(carrotPos);

      // Increment score when carrot is collected
      this.updateScore();
    }
  }

  explode(pos) {
    this.isExploding = true;
    let p1Count = this.particles1.length;
    let p2Count = this.particles2.length;
    let i = 0;
    for (i = 0; i < p1Count; i++) {
      let m = this.particles1[i];
      m.position.x = pos.x;
      m.position.y = pos.y;
      m.position.z = pos.z;
      m.scale.set(2, 2, 2);

      gsap.to(m.position, {
        x: pos.x + (-0.5 + Math.random()) * 1.5,
        y: pos.y + (0.5 + Math.random()) * 1.5,
        z: pos.z + (-0.5 + Math.random()) * 1.5,
        duration: 1,
        ease: "power4.out",
      });
      gsap.to(m.scale, {
        x: 0,
        y: 0,
        z: 0,
        duration: 1,
        ease: "power4.out",
        onComplete: () => {
          if (this.gameActive) {
            this.spawnCarrot();
          }
          this.isExploding = false;
        },
      });
    }
    for (i = 0; i < p2Count; i++) {
      let m = this.particles2[i];
      m.position.x = pos.x;
      m.position.y = pos.y;
      m.position.z = pos.z;
      m.scale.set(2, 2, 2);

      gsap.to(m.position, {
        x: pos.x + (-0.5 + Math.random()) * 1.5,
        y: pos.y + (0.5 + Math.random()) * 1.5,
        z: pos.z + (-0.5 + Math.random()) * 1.5,
        duration: 1,
        ease: "power4.out",
      });
      gsap.to(m.scale, {
        x: 0,
        y: 0,
        z: 0,
        duration: 1,
        ease: "power4.out",
      });
    }
  }

  spawnCarrot() {
    // Only spawn if the game is active
    if (!this.gameActive) return;

    // Calculate safe boundaries to ensure carrot is within visible area
    // Use 80% of floor size to keep away from edges
    const safeRadius = this.floorSize * 0.4;
    const px = ((Math.random() - 0.5) * safeRadius) / this.floorSize;
    const py = ((Math.random() - 0.5) * safeRadius) / this.floorSize;
    const h = 0.2 + Math.random() * 1;

    // Ensure carrot is inside the playable area
    this.carrot.position.x = px * this.floorSize;
    this.carrot.position.z = py * this.floorSize;
    this.carrot.position.y = -1;

    this.carrot.scale.set(0, 0, 0);
    this.carrot.visible = true;

    gsap.to(this.carrot.scale, {
      duration: 1.5,
      ease: "elastic.out",
      x: 1,
      y: 1,
      z: 1,
    });
    gsap.to(this.carrot.position, {
      duration: 1.5,
      ease: "elastic.out",
      y: h,
    });
  }

  modifyFloorShader() {
    let renderTarget = this.floor.getRenderTarget();
    const textureMatrix = this.floor.material.uniforms.textureMatrix;

    const fragmentShader = document.getElementById(
      "reflectorFragmentShader"
    ).textContent;
    const vertexShader = document.getElementById(
      "reflectorVertexShader"
    ).textContent;

    const uniforms = THREE.UniformsUtils.merge([
      THREE.UniformsLib["common"],
      THREE.UniformsLib["shadowmap"],
      THREE.UniformsLib["lights"],
      this.floor.material.uniforms,
      {
        tScratches: { value: this.bufferSim.output.texture },
      },
    ]);

    this.floor.material.lights = true;
    this.floor.material.uniforms = uniforms;
    this.floor.material.uniforms.tDiffuse.value = renderTarget.texture;
    this.floor.material.uniforms.textureMatrix.value =
      textureMatrix.value;
    this.floor.material.vertexShader = vertexShader;
    this.floor.material.fragmentShader = fragmentShader;
  }
}

class BufferSim {
  constructor(renderer, width, height, shader) {
    this.renderer = renderer;
    this.shader = shader;
    this.orthoScene = new THREE.Scene();
    var fbo = new THREE.WebGLRenderTarget(width, height, {
      wrapS: THREE.ClampToEdgeWrapping,
      wrapT: THREE.ClampToEdgeWrapping,
      minFilter: THREE.LinearFilter,
      magFilter: THREE.LinearFilter,
      format: THREE.RGBAFormat,
      type: THREE.FloatType,
      stencilBuffer: false,
      depthBuffer: false,
    });

    fbo.texture.generateMipmaps = false;

    this.fbos = [fbo, fbo.clone()];
    this.current = 0;
    this.output = this.fbos[0];
    this.orthoCamera = new THREE.OrthographicCamera(
      width / -2,
      width / 2,
      height / 2,
      height / -2,
      0.00001,
      1000
    );
    this.orthoQuad = new THREE.Mesh(
      new THREE.PlaneGeometry(width, height),
      this.shader
    );
    this.orthoScene.add(this.orthoQuad);
  }

  render() {
    this.shader.uniforms.inputTexture.value =
      this.fbos[this.current].texture;
    this.input = this.fbos[this.current];
    this.current = 1 - this.current;
    this.output = this.fbos[this.current];
    this.renderer.setRenderTarget(this.output);
    this.renderer.render(this.orthoScene, this.orthoCamera);
    this.renderer.setRenderTarget(null);
  }
}

// Initialize the game when the page loads
document.addEventListener("DOMContentLoaded", () => new App());

Comments

Popular Posts

Offline Page

15 August Code

Portfolio Website