猩球大战

Published on
/
/趣玩前端
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>猩球大战</title>
    <style>
      @import url("https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap");

      body {
        margin: 0;
        padding: 0;
        font-family: "Inconsolata", monospace;
        font-size: 14px;
        color: white;
        user-select: none;
        -webkit-user-select: none;

        display: flex;
        justify-content: center;
        align-items: center;
        height: 100%;
        overflow: hidden;
      }

      button {
        cursor: pointer;
        border: none;
        color: white;
        background: transparent;
        font-family: "Inconsolata", monospace;
        padding: 10px;
        font-size: 1em;
      }

      button:hover {
        background-color: rgba(255, 255, 255, 0.1);
      }

      #info-left,
      #info-right {
        position: absolute;
        top: 20px;
      }

      #info-left {
        left: 25px;
      }

      #info-right {
        right: 25px;
        text-align: right;
      }

      #bomb-grab-area {
        position: absolute;
        width: 30px;
        height: 30px;
        border-radius: 50%;
        background-color: transparent;
        cursor: grab;
      }

      #instructions,
      #congratulations {
        position: absolute;
        transition: visibility 0s, opacity 0.5s linear;
      }

      @media (min-height: 535px) {
        #instructions {
          min-height: 200px;
        }
      }

      #congratulations {
        background-color: rgba(255, 255, 255, 0.9);
        color: black;
        padding: 50px 80px;
        opacity: 0;
        visibility: hidden;
        max-width: 300px;
        backdrop-filter: blur(5px);
      }

      #congratulations p a {
        color: inherit;
      }

      #congratulations button {
        border: 1px solid rgba(0, 0, 0, 0.9);
        color: inherit;
      }

      #settings {
        position: absolute;
        top: calc(20px + 16.385px - 10px);
        display: flex;
        align-items: center;
        gap: 10px;
        right: 11em;
      }

      #settings,
      #info-left,
      #info-right {
        opacity: 0;
        transition: opacity 3s;
      }

      @media (max-width: 450px) {
        #settings,
        #info-left,
        #info-right {
          opacity: 0;
        }
        #instructions {
          visibility: hidden;
        }
      }

      /* Basic CSS for the dropdown */
      .dropdown {
        position: relative;
        display: inline-block;
      }

      .dropbtn:after {
        content: "▼";
        margin-left: 7px;
        font-size: 0.8em;
        vertical-align: text-top;
      }

      .dropdown-content {
        display: none;
        position: absolute;
        background-color: #f9f9f9;
        min-width: 120px;
        box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
      }

      .dropdown-content a {
        color: black;
        padding: 12px 16px;
        text-decoration: none;
        display: block;
        white-space: nowrap;
        font-size: 0.9em;
      }

      .dropdown-content a:hover {
        background-color: #f1f1f1;
      }

      /* Show dropdown content when hovering over the button */
      .dropdown:hover .dropdown-content {
        display: block;
      }

      #windmill {
        position: absolute;
        right: 0;
        fill: rgba(255, 255, 255, 0.5);
        transform-origin: bottom;
      }

      #windmill-head {
        animation-name: rotate;
        animation-duration: 4s;
        animation-iteration-count: infinite;
        animation-timing-function: linear;
      }

      @keyframes rotate {
        from {
          transform: rotate(0deg);
        }
        to {
          transform: rotate(360deg);
        }
      }

      #wind-info {
        position: absolute;
        width: 100px;
        text-align: center;
        margin-bottom: 30px;
      }

      /** Youtube logo by https://codepen.io/alvaromontoro */
      #youtube {
        z-index: 2;
        display: block;
        width: 30px;
        height: 21px;
        background: red;
        position: relative;
        border-radius: 50% / 11%;
        transition: transform 0.5s;
        color: black;
      }

      #youtube:hover,
      #youtube:focus {
        transform: scale(1.2);
      }

      #youtube::before {
        content: "";
        display: block;
        position: absolute;
        top: 7.5%;
        left: -6%;
        width: 112%;
        height: 85%;
        background: red;
        border-radius: 9% / 50%;
      }

      #youtube::after {
        content: "";
        display: block;
        position: absolute;
        top: 6px;
        left: 11px;
        width: 15px;
        height: 10px;
        border: 5px solid transparent;
        box-sizing: border-box;
        border-left: 10px solid white;
      }

      #youtube span {
        font-size: 0;
        position: absolute;
        width: 0;
        height: 0;
        overflow: hidden;
      }

      #youtube-card {
        display: none;
      }

      #youtube:hover + #youtube-card {
        color: black;
        display: block;
        position: absolute;
        top: -20px;
        right: -20px;
        padding: 25px 60px 25px 25px;
        width: 200px;
        background-color: white;
      }

      #fullscreen {
        all: unset;
        cursor: pointer;
        position: absolute;
        right: 10px;
        bottom: 10px;
      }
    </style>
  </head>
  <body>
    <canvas id="game"></canvas>

    <svg width="200" height="250" id="windmill">
      <defs>
        <path id="arm" d="M -7 -20 C -7 -10 7 -10 7 -20 L 2 -80 L -2 -80" />
      </defs>
      <g transform="translate(100, 100)">
        <g id="windmill-head">
          <circle r="8"></circle>
          <use href="#arm" />
          <use href="#arm" transform="rotate(+120)" />
          <use href="#arm" transform="rotate(-120)" />
        </g>
      </g>
      <path
        transform="translate(100, 0)"
        d="M -7 250 L 7 250 L 3 115 L -3 115"
      ></path>
    </svg>

    <div id="wind-info">风速: <span id="wind-speed">0</span></div>

    <div id="info-left">
      <h3><span class="name">玩家</span></h3>
      <p>角度: <span class="angle">0</span>°</p>
      <p>速度: <span class="velocity">0</span></p>
    </div>

    <div id="info-right">
      <h3><span class="name">电脑</span></h3>
      <p>角度: <span class="angle">0</span>°</p>
      <p>速度: <span class="velocity">0</span></p>
    </div>

    <div id="instructions">
      <h3 id="game-mode">玩家 vs. 电脑</h3>
      <h1>拖动炸弹瞄准!</h1>
    </div>

    <div id="bomb-grab-area"></div>

    <div id="congratulations">
      <h1><span id="winner">?</span> 赢了!</h1>
      <div class="dropdown">
        <button class="dropbtn">新游戏</button>
        <div class="dropdown-content">
          <a href="#" class="single-player">单人模式</a>
          <a href="#" class="two-players">双人模式</a>
          <a href="#" class="auto-play">自动播放</a>
        </div>
      </div>
    </div>

    <div id="settings">
      <div class="dropdown">
        <button class="dropbtn">新游戏</button>
        <div class="dropdown-content">
          <a href="#" class="single-player">单人模式</a>
          <a href="#" class="two-players">双人模式</a>
          <a href="#" class="auto-play">自动播放</a>
        </div>
      </div>

      <button id="color-mode">暗色模式</button>
    </div>

    <button id="fullscreen" onclick="toggleFullscreen()">
      <svg width="30" height="30">
        <path
          id="enter-fullscreen"
          stroke="white"
          stroke-width="3"
          fill="none"
          d="
             M 10, 2 L 2,2 L 2, 10
             M 20, 2 L 28,2 L 28, 10
             M 28, 20 L 28,28 L 20, 28
             M 10, 28 L 2,28 L 2, 20"
        />
        <path
          id="exit-fullscreen"
          stroke="transparent"
          stroke-width="3"
          fill="none"
          d="
             M 10, 2 L 10,10 L 2, 10
             M 20, 2 L 20,10 L 28, 10
             M 28, 20 L 20,20 L 20, 28
             M 10, 28 L 10,20 L 2, 20"
        />
      </svg>
    </button>
    <!-- partial -->
    <script>
      // The state of the game
      let state = {};

      let isDragging = false;
      let dragStartX = undefined;
      let dragStartY = undefined;

      let previousAnimationTimestamp = undefined;
      let animationFrameRequestID = undefined;
      let delayTimeoutID = undefined;

      let simulationMode = false;
      let simulationImpact = {};

      const darkModeMediaQuery = window.matchMedia(
        "(prefers-color-scheme: dark)"
      );

      // Settings
      const settings = {
        numberOfPlayers: 1, // 0 means two computers are playing against each other
        mode: darkModeMediaQuery.matches ? "dark" : "light",
      };

      const blastHoleRadius = 18;

      // The main canvas element and its drawing context
      const canvas = document.getElementById("game");
      canvas.width = window.innerWidth * window.devicePixelRatio;
      canvas.height = window.innerHeight * window.devicePixelRatio;
      canvas.style.width = window.innerWidth + "px";
      canvas.style.height = window.innerHeight + "px";
      const ctx = canvas.getContext("2d");

      // Windmill
      const windmillDOM = document.getElementById("windmill");
      const windmillHeadDOM = document.getElementById("windmill-head");
      const windInfoDOM = document.getElementById("wind-info");
      const windSpeedDOM = document.getElementById("wind-speed");

      // Left info panel
      const info1DOM = document.getElementById("info-left");
      const name1DOM = document.querySelector("#info-left .name");
      const angle1DOM = document.querySelector("#info-left .angle");
      const velocity1DOM = document.querySelector("#info-left .velocity");

      // Right info panel
      const info2DOM = document.getElementById("info-right");
      const name2DOM = document.querySelector("#info-right .name");
      const angle2DOM = document.querySelector("#info-right .angle");
      const velocity2DOM = document.querySelector("#info-right .velocity");

      // Instructions panel
      const instructionsDOM = document.getElementById("instructions");
      const gameModeDOM = document.getElementById("game-mode");

      // The bomb's grab area
      const bombGrabAreaDOM = document.getElementById("bomb-grab-area");

      // Congratulations panel
      const congratulationsDOM = document.getElementById("congratulations");
      const winnerDOM = document.getElementById("winner");

      // Settings toolbar
      const settingsDOM = document.getElementById("settings");
      const singlePlayerButtonDOM = document.querySelectorAll(".single-player");
      const twoPlayersButtonDOM = document.querySelectorAll(".two-players");
      const autoPlayButtonDOM = document.querySelectorAll(".auto-play");
      const colorModeButtonDOM = document.getElementById("color-mode");

      colorModeButtonDOM.addEventListener("click", () => {
        if (settings.mode === "dark") {
          settings.mode = "light";
          colorModeButtonDOM.innerText = "暗色模式";
        } else {
          settings.mode = "dark";
          colorModeButtonDOM.innerText = "亮色模式";
        }
        draw();
      });

      darkModeMediaQuery.addEventListener("change", (e) => {
        settings.mode = e.matches ? "dark" : "light";
        if (settings.mode === "dark") {
          colorModeButtonDOM.innerText = "亮色模式";
        } else {
          colorModeButtonDOM.innerText = "暗色模式";
        }
        draw();
      });

      newGame();

      function newGame() {
        // Reset game state
        state = {
          phase: "aiming", // aiming | in flight | celebrating
          currentPlayer: 1,
          round: 1,
          windSpeed: generateWindSpeed(),
          bomb: {
            x: undefined,
            y: undefined,
            rotation: 0,
            velocity: { x: 0, y: 0 },
            highlight: true,
          },

          // Buildings
          backgroundBuildings: [],
          buildings: [],
          blastHoles: [],

          stars: [],

          scale: 1,
          shift: 0,
        };

        // Generate stars
        for (
          let i = 0;
          i < (window.innerWidth * window.innerHeight) / 12000;
          i++
        ) {
          const x = Math.floor(Math.random() * window.innerWidth);
          const y = Math.floor(Math.random() * window.innerHeight);
          state.stars.push({ x, y });
        }

        // Generate background buildings
        for (let i = 0; i < 17; i++) {
          generateBackgroundBuilding(i);
        }

        // Generate buildings
        for (let i = 0; i < 8; i++) {
          generateBuilding(i);
        }

        calculateScaleAndShift();
        initializeBombPosition();
        initializeWindmillPosition();
        setWindMillRotation();

        // Cancel any ongoing animation and timeout
        cancelAnimationFrame(animationFrameRequestID);
        clearTimeout(delayTimeoutID);

        // Reset HTML elements
        if (settings.numberOfPlayers > 0) {
          showInstructions();
        } else {
          hideInstructions();
        }
        hideCongratulations();
        angle1DOM.innerText = 0;
        velocity1DOM.innerText = 0;
        angle2DOM.innerText = 0;
        velocity2DOM.innerText = 0;

        // Reset simulation mode
        simulationMode = false;
        simulationImpact = {};

        draw();

        if (settings.numberOfPlayers === 0) {
          computerThrow();
        }
      }

      function showInstructions() {
        singlePlayerButtonDOM.checked = true;
        instructionsDOM.style.opacity = 1;
        instructionsDOM.style.visibility = "visible";
      }

      function hideInstructions() {
        state.bomb.highlight = false;
        instructionsDOM.style.opacity = 0;
        instructionsDOM.style.visibility = "hidden";
      }

      function showCongratulations() {
        congratulationsDOM.style.opacity = 1;
        congratulationsDOM.style.visibility = "visible";
      }

      function hideCongratulations() {
        congratulationsDOM.style.opacity = 0;
        congratulationsDOM.style.visibility = "hidden";
      }

      function generateBackgroundBuilding(index) {
        const previousBuilding = state.backgroundBuildings[index - 1];

        const x = previousBuilding
          ? previousBuilding.x + previousBuilding.width + 4
          : -300;

        const minWidth = 60;
        const maxWidth = 110;
        const width = minWidth + Math.random() * (maxWidth - minWidth);

        const smallerBuilding = index < 4 || index >= 13;

        const minHeight = 80;
        const maxHeight = 350;
        const smallMinHeight = 20;
        const smallMaxHeight = 150;
        const height = smallerBuilding
          ? smallMinHeight + Math.random() * (smallMaxHeight - smallMinHeight)
          : minHeight + Math.random() * (maxHeight - minHeight);

        state.backgroundBuildings.push({ x, width, height });
      }

      function generateBuilding(index) {
        const previousBuilding = state.buildings[index - 1];

        const x = previousBuilding
          ? previousBuilding.x + previousBuilding.width + 4
          : 0;

        const minWidth = 80;
        const maxWidth = 130;
        const width = minWidth + Math.random() * (maxWidth - minWidth);

        const smallerBuilding = index <= 1 || index >= 6;

        const minHeight = 40;
        const maxHeight = 300;
        const minHeightGorilla = 30;
        const maxHeightGorilla = 150;

        const height = smallerBuilding
          ? minHeightGorilla +
            Math.random() * (maxHeightGorilla - minHeightGorilla)
          : minHeight + Math.random() * (maxHeight - minHeight);

        // Generate an array of booleans to show if the light is on or off in a room
        const lightsOn = [];
        for (let i = 0; i < 50; i++) {
          const light = Math.random() <= 0.33 ? true : false;
          lightsOn.push(light);
        }

        state.buildings.push({ x, width, height, lightsOn });
      }

      function calculateScaleAndShift() {
        const lastBuilding = state.buildings.at(-1);
        const totalWidthOfTheCity = lastBuilding.x + lastBuilding.width;

        const horizontalScale = window.innerWidth / totalWidthOfTheCity ?? 1;
        const verticalScale = window.innerHeight / 500;

        state.scale = Math.min(horizontalScale, verticalScale);

        const sceneNeedsToBeShifted = horizontalScale > verticalScale;

        state.shift = sceneNeedsToBeShifted
          ? (window.innerWidth - totalWidthOfTheCity * state.scale) / 2
          : 0;
      }

      window.addEventListener("resize", () => {
        canvas.width = window.innerWidth * window.devicePixelRatio;
        canvas.height = window.innerHeight * window.devicePixelRatio;
        canvas.style.width = window.innerWidth + "px";
        canvas.style.height = window.innerHeight + "px";
        calculateScaleAndShift();
        initializeBombPosition();
        initializeWindmillPosition();
        draw();
      });

      function initializeBombPosition() {
        const building =
          state.currentPlayer === 1
            ? state.buildings.at(1) // Second building
            : state.buildings.at(-2); // Second last building

        const gorillaX = building.x + building.width / 2;
        const gorillaY = building.height;

        const gorillaHandOffsetX = state.currentPlayer === 1 ? -28 : 28;
        const gorillaHandOffsetY = 107;

        state.bomb.x = gorillaX + gorillaHandOffsetX;
        state.bomb.y = gorillaY + gorillaHandOffsetY;
        state.bomb.velocity.x = 0;
        state.bomb.velocity.y = 0;
        state.bomb.rotation = 0;

        // Initialize the position of the grab area in HTML
        const grabAreaRadius = 15;
        const left = state.bomb.x * state.scale + state.shift - grabAreaRadius;
        const bottom = state.bomb.y * state.scale - grabAreaRadius;

        bombGrabAreaDOM.style.left = `${left}px`;
        bombGrabAreaDOM.style.bottom = `${bottom}px`;
      }

      function initializeWindmillPosition() {
        // Move windmill into position
        const lastBuilding = state.buildings.at(-1);
        let rooftopY = lastBuilding.height * state.scale;
        let rooftopX =
          (lastBuilding.x + lastBuilding.width / 2) * state.scale + state.shift;

        windmillDOM.style.bottom = `${rooftopY}px`;
        windmillDOM.style.left = `${rooftopX - 100}px`;

        windmillDOM.style.scale = state.scale;

        windInfoDOM.style.bottom = `${rooftopY}px`;
        windInfoDOM.style.left = `${rooftopX - 50}px`;
      }

      function draw() {
        ctx.save();

        ctx.scale(window.devicePixelRatio, window.devicePixelRatio);

        drawBackgroundSky();

        // Flip coordinate system upside down
        ctx.translate(0, window.innerHeight);
        ctx.scale(1, -1);

        // Scale and shift view to center
        ctx.translate(state.shift, 0);
        ctx.scale(state.scale, state.scale);

        // Draw scene
        drawBackgroundMoon();
        drawBackgroundBuildings();
        drawBuildingsWithBlastHoles();
        drawGorilla(1);
        drawGorilla(2);
        drawBomb();

        // Restore transformation
        ctx.restore();
      }

      function drawBackgroundSky() {
        const gradient = ctx.createLinearGradient(0, 0, 0, window.innerHeight);
        if (settings.mode === "dark") {
          gradient.addColorStop(1, "#27507F");
          gradient.addColorStop(0, "#58A8D8");
        } else {
          gradient.addColorStop(1, "#F8BA85");
          gradient.addColorStop(0, "#FFC28E");
        }

        // Draw sky
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);

        // Draw stars
        if (settings.mode === "dark") {
          ctx.fillStyle = "white";
          state.stars.forEach((star) => {
            ctx.fillRect(star.x, star.y, 1, 1);
          });
        }
      }

      function drawBackgroundMoon() {
        if (settings.mode === "dark") {
          ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
          ctx.beginPath();
          ctx.arc(
            window.innerWidth / state.scale - state.shift - 200,
            window.innerHeight / state.scale - 100,
            30,
            0,
            2 * Math.PI
          );
          ctx.fill();
        } else {
          ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
          ctx.beginPath();
          ctx.arc(300, 350, 60, 0, 2 * Math.PI);
          ctx.fill();
        }
      }

      function drawBackgroundBuildings() {
        state.backgroundBuildings.forEach((building) => {
          ctx.fillStyle = settings.mode === "dark" ? "#254D7E" : "#947285";
          ctx.fillRect(building.x, 0, building.width, building.height);
        });
      }

      function drawBuildingsWithBlastHoles() {
        ctx.save();

        state.blastHoles.forEach((blastHole) => {
          ctx.beginPath();

          // Outer shape clockwise
          ctx.rect(
            0,
            0,
            window.innerWidth / state.scale,
            window.innerHeight / state.scale
          );

          // Inner shape counterclockwise
          ctx.arc(
            blastHole.x,
            blastHole.y,
            blastHoleRadius,
            0,
            2 * Math.PI,
            true
          );

          ctx.clip();
        });

        drawBuildings();

        ctx.restore();
      }

      function drawBuildings() {
        state.buildings.forEach((building) => {
          // Draw building
          ctx.fillStyle = settings.mode === "dark" ? "#152A47" : "#4A3C68";
          ctx.fillRect(building.x, 0, building.width, building.height);

          // Draw windows
          const windowWidth = 10;
          const windowHeight = 12;
          const gap = 15;

          const numberOfFloors = Math.ceil(
            (building.height - gap) / (windowHeight + gap)
          );
          const numberOfRoomsPerFloor = Math.floor(
            (building.width - gap) / (windowWidth + gap)
          );

          for (let floor = 0; floor < numberOfFloors; floor++) {
            for (let room = 0; room < numberOfRoomsPerFloor; room++) {
              if (building.lightsOn[floor * numberOfRoomsPerFloor + room]) {
                ctx.save();

                ctx.translate(building.x + gap, building.height - gap);
                ctx.scale(1, -1);

                const x = room * (windowWidth + gap);
                const y = floor * (windowHeight + gap);

                ctx.fillStyle =
                  settings.mode === "dark" ? "#5F76AB" : "#EBB6A2";
                ctx.fillRect(x, y, windowWidth, windowHeight);

                ctx.restore();
              }
            }
          }
        });
      }

      function drawGorilla(player) {
        ctx.save();

        const building =
          player === 1
            ? state.buildings.at(1) // Second building
            : state.buildings.at(-2); // Second last building

        ctx.translate(building.x + building.width / 2, building.height);

        drawGorillaBody();
        drawGorillaLeftArm(player);
        drawGorillaRightArm(player);
        drawGorillaFace(player);
        drawGorillaThoughtBubbles(player);

        ctx.restore();
      }

      function drawGorillaBody() {
        ctx.fillStyle = "black";

        ctx.beginPath();
        ctx.moveTo(0, 15);
        ctx.lineTo(-7, 0);
        ctx.lineTo(-20, 0);
        ctx.lineTo(-17, 18);
        ctx.lineTo(-20, 44);

        ctx.lineTo(-11, 77);
        ctx.lineTo(0, 84);
        ctx.lineTo(11, 77);

        ctx.lineTo(20, 44);
        ctx.lineTo(17, 18);
        ctx.lineTo(20, 0);
        ctx.lineTo(7, 0);
        ctx.fill();
      }

      function drawGorillaLeftArm(player) {
        ctx.strokeStyle = "black";
        ctx.lineWidth = 18;

        ctx.beginPath();
        ctx.moveTo(-14, 50);

        if (
          state.phase === "aiming" &&
          state.currentPlayer === 1 &&
          player === 1
        ) {
          ctx.quadraticCurveTo(
            -44,
            63,
            -28 - state.bomb.velocity.x / 6.25,
            107 - state.bomb.velocity.y / 6.25
          );
        } else if (
          state.phase === "celebrating" &&
          state.currentPlayer === player
        ) {
          ctx.quadraticCurveTo(-44, 63, -28, 107);
        } else {
          ctx.quadraticCurveTo(-44, 45, -28, 12);
        }

        ctx.stroke();
      }

      function drawGorillaRightArm(player) {
        ctx.strokeStyle = "black";
        ctx.lineWidth = 18;

        ctx.beginPath();
        ctx.moveTo(+14, 50);

        if (
          state.phase === "aiming" &&
          state.currentPlayer === 2 &&
          player === 2
        ) {
          ctx.quadraticCurveTo(
            +44,
            63,
            +28 - state.bomb.velocity.x / 6.25,
            107 - state.bomb.velocity.y / 6.25
          );
        } else if (
          state.phase === "celebrating" &&
          state.currentPlayer === player
        ) {
          ctx.quadraticCurveTo(+44, 63, +28, 107);
        } else {
          ctx.quadraticCurveTo(+44, 45, +28, 12);
        }

        ctx.stroke();
      }

      function drawGorillaFace(player) {
        // Face
        ctx.fillStyle = settings.mode === "dark" ? "gray" : "lightgray";
        ctx.beginPath();
        ctx.arc(0, 63, 9, 0, 2 * Math.PI);
        ctx.moveTo(-3.5, 70);
        ctx.arc(-3.5, 70, 4, 0, 2 * Math.PI);
        ctx.moveTo(+3.5, 70);
        ctx.arc(+3.5, 70, 4, 0, 2 * Math.PI);
        ctx.fill();

        // Eyes
        ctx.fillStyle = "black";
        ctx.beginPath();
        ctx.arc(-3.5, 70, 1.4, 0, 2 * Math.PI);
        ctx.moveTo(+3.5, 70);
        ctx.arc(+3.5, 70, 1.4, 0, 2 * Math.PI);
        ctx.fill();

        ctx.strokeStyle = "black";
        ctx.lineWidth = 1.4;

        // Nose
        ctx.beginPath();
        ctx.moveTo(-3.5, 66.5);
        ctx.lineTo(-1.5, 65);
        ctx.moveTo(3.5, 66.5);
        ctx.lineTo(1.5, 65);
        ctx.stroke();

        // Mouth
        ctx.beginPath();
        if (state.phase === "celebrating" && state.currentPlayer === player) {
          ctx.moveTo(-5, 60);
          ctx.quadraticCurveTo(0, 56, 5, 60);
        } else {
          ctx.moveTo(-5, 56);
          ctx.quadraticCurveTo(0, 60, 5, 56);
        }
        ctx.stroke();
      }

      function drawGorillaThoughtBubbles(player) {
        if (state.phase === "aiming") {
          const currentPlayerIsComputer =
            (settings.numberOfPlayers === 0 &&
              state.currentPlayer === 1 &&
              player === 1) ||
            (settings.numberOfPlayers !== 2 &&
              state.currentPlayer === 2 &&
              player === 2);

          if (currentPlayerIsComputer) {
            ctx.save();
            ctx.scale(1, -1);

            ctx.font = "20px sans-serif";
            ctx.textAlign = "center";
            ctx.fillText("?", 0, -90);

            ctx.font = "10px sans-serif";

            ctx.rotate((5 / 180) * Math.PI);
            ctx.fillText("?", 0, -90);

            ctx.rotate((-10 / 180) * Math.PI);
            ctx.fillText("?", 0, -90);

            ctx.restore();
          }
        }
      }

      function drawBomb() {
        ctx.save();
        ctx.translate(state.bomb.x, state.bomb.y);

        if (state.phase === "aiming") {
          // Move the bomb with the mouse while aiming
          ctx.translate(
            -state.bomb.velocity.x / 6.25,
            -state.bomb.velocity.y / 6.25
          );

          // Draw throwing trajectory
          ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
          ctx.setLineDash([3, 8]);
          ctx.lineWidth = 3;
          ctx.beginPath();
          ctx.moveTo(0, 0);
          ctx.lineTo(state.bomb.velocity.x, state.bomb.velocity.y);
          ctx.stroke();

          // Draw circle
          ctx.fillStyle = "white";
          ctx.beginPath();
          ctx.arc(0, 0, 6, 0, 2 * Math.PI);
          ctx.fill();
        } else if (state.phase === "in flight") {
          // Draw rotated banana
          ctx.fillStyle = "white";
          ctx.rotate(state.bomb.rotation);
          ctx.beginPath();
          ctx.moveTo(-8, -2);
          ctx.quadraticCurveTo(0, 12, 8, -2);
          ctx.quadraticCurveTo(0, 2, -8, -2);
          ctx.fill();
        } else {
          // Draw circle
          ctx.fillStyle = "white";
          ctx.beginPath();
          ctx.arc(0, 0, 6, 0, 2 * Math.PI);
          ctx.fill();
        }

        // Restore transformation
        ctx.restore();

        // Indicator showing if the bomb is above the screen
        if (state.bomb.y > window.innerHeight / state.scale) {
          ctx.beginPath();
          ctx.strokeStyle = "white";
          const distance = state.bomb.y - window.innerHeight / state.scale;
          ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10);
          ctx.lineTo(state.bomb.x, window.innerHeight / state.scale - distance);
          ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10);
          ctx.lineTo(state.bomb.x - 5, window.innerHeight / state.scale - 15);
          ctx.moveTo(state.bomb.x, window.innerHeight / state.scale - 10);
          ctx.lineTo(state.bomb.x + 5, window.innerHeight / state.scale - 15);
          ctx.stroke();
        }

        // Indicator showing the starting position of the bomb
        if (state.bomb.highlight) {
          ctx.beginPath();
          ctx.strokeStyle = "white";
          ctx.lineWidth = 2;
          ctx.moveTo(state.bomb.x, state.bomb.y + 20);
          ctx.lineTo(state.bomb.x, state.bomb.y + 120);
          ctx.moveTo(state.bomb.x, state.bomb.y + 20);
          ctx.lineTo(state.bomb.x - 5, state.bomb.y + 25);
          ctx.moveTo(state.bomb.x, state.bomb.y + 20);
          ctx.lineTo(state.bomb.x + 5, state.bomb.y + 25);
          ctx.stroke();
        }
      }

      // Event handlers
      bombGrabAreaDOM.addEventListener("mousedown", function (e) {
        hideInstructions();
        if (state.phase === "aiming") {
          isDragging = true;
          dragStartX = e.clientX;
          dragStartY = e.clientY;
          document.body.style.cursor = "grabbing";
        }
      });

      window.addEventListener("mousemove", function (e) {
        if (isDragging) {
          let deltaX = e.clientX - dragStartX;
          let deltaY = e.clientY - dragStartY;

          state.bomb.velocity.x = -deltaX;
          state.bomb.velocity.y = deltaY;
          setInfo(deltaX, deltaY);

          draw();
        }
      });

      // Set values on the info panel
      function setInfo(deltaX, deltaY) {
        const hypotenuse = Math.sqrt(deltaX ** 2 + deltaY ** 2);
        const angleInRadians = Math.asin(deltaY / hypotenuse);
        const angleInDegrees = (angleInRadians / Math.PI) * 180;

        if (state.currentPlayer === 1) {
          angle1DOM.innerText = Math.round(angleInDegrees);
          velocity1DOM.innerText = Math.round(hypotenuse);
        } else {
          angle2DOM.innerText = Math.round(angleInDegrees);
          velocity2DOM.innerText = Math.round(hypotenuse);
        }
      }

      window.addEventListener("mouseup", function () {
        if (isDragging) {
          isDragging = false;
          document.body.style.cursor = "default";

          throwBomb();
        }
      });

      function computerThrow() {
        const numberOfSimulations = 2 + state.round * 3;
        const bestThrow = runSimulations(numberOfSimulations);

        initializeBombPosition();
        state.bomb.velocity.x = bestThrow.velocityX;
        state.bomb.velocity.y = bestThrow.velocityY;
        setInfo(bestThrow.velocityX, bestThrow.velocityY);

        // Draw the aiming gorilla
        draw();

        // Make it look like the computer is thinking for a second
        delayTimeoutID = setTimeout(throwBomb, 1000);
      }

      // Simulate multiple throws and pick the best
      function runSimulations(numberOfSimulations) {
        let bestThrow = {
          velocityX: undefined,
          velocityY: undefined,
          distance: Infinity,
        };
        simulationMode = true;

        // Calculating the center position of the enemy
        const enemyBuilding =
          state.currentPlayer === 1
            ? state.buildings.at(-2) // Second last building
            : state.buildings.at(1); // Second building
        const enemyX = enemyBuilding.x + enemyBuilding.width / 2;
        const enemyY = enemyBuilding.height + 30;

        for (let i = 0; i < numberOfSimulations; i++) {
          // Pick a random angle and velocity
          const angleInDegrees = -10 + Math.random() * 100;
          const angleInRadians = (angleInDegrees / 180) * Math.PI;
          const velocity = 40 + Math.random() * 130;

          // Calculate the horizontal and vertical velocity
          const direction = state.currentPlayer === 1 ? 1 : -1;
          const velocityX = Math.cos(angleInRadians) * velocity * direction;
          const velocityY = Math.sin(angleInRadians) * velocity;

          initializeBombPosition();
          state.bomb.velocity.x = velocityX;
          state.bomb.velocity.y = velocityY;

          throwBomb();

          // Calculating the distance between the simulated impact and the enemy
          const distance = Math.sqrt(
            (enemyX - simulationImpact.x) ** 2 +
              (enemyY - simulationImpact.y) ** 2
          );

          // If the current impact is closer to the enemy
          // than any of the previous simulations then pick this one
          if (distance < bestThrow.distance) {
            bestThrow = { velocityX, velocityY, distance };
          }
        }

        simulationMode = false;
        return bestThrow;
      }

      function throwBomb() {
        if (simulationMode) {
          previousAnimationTimestamp = 0;
          animate(16);
        } else {
          state.phase = "in flight";
          previousAnimationTimestamp = undefined;
          animationFrameRequestID = requestAnimationFrame(animate);
        }
      }

      function animate(timestamp) {
        if (previousAnimationTimestamp === undefined) {
          previousAnimationTimestamp = timestamp;
          animationFrameRequestID = requestAnimationFrame(animate);
          return;
        }

        const elapsedTime = timestamp - previousAnimationTimestamp;

        // We break down every animation cycle into 10 tiny movements for greater hit detection precision
        const hitDetectionPrecision = 10;
        for (let i = 0; i < hitDetectionPrecision; i++) {
          moveBomb(elapsedTime / hitDetectionPrecision);

          // Hit detection
          const miss = checkFrameHit() || checkBuildingHit(); // Bomb got off-screen or hit a building
          const hit = checkGorillaHit(); // Bomb hit the enemy

          if (simulationMode && (hit || miss)) {
            simulationImpact = { x: state.bomb.x, y: state.bomb.y };
            return; // Simulation ended, return from the loop
          }

          // Handle the case when we hit a building or the bomb got off-screen
          if (miss) {
            state.currentPlayer = state.currentPlayer === 1 ? 2 : 1; // Switch players
            if (state.currentPlayer === 1) state.round++;
            state.phase = "aiming";
            initializeBombPosition();

            draw();

            const computerThrowsNext =
              settings.numberOfPlayers === 0 ||
              (settings.numberOfPlayers === 1 && state.currentPlayer === 2);

            if (computerThrowsNext) setTimeout(computerThrow, 50);

            return;
          }

          // Handle the case when we hit the enemy
          if (hit) {
            state.phase = "celebrating";
            announceWinner();

            draw();
            return;
          }
        }

        if (!simulationMode) draw();

        // Continue the animation loop
        previousAnimationTimestamp = timestamp;
        if (simulationMode) {
          animate(timestamp + 16);
        } else {
          animationFrameRequestID = requestAnimationFrame(animate);
        }
      }

      function moveBomb(elapsedTime) {
        const multiplier = elapsedTime / 200;

        // Adjust trajectory by wind
        state.bomb.velocity.x += state.windSpeed * multiplier;

        // Adjust trajectory by gravity
        state.bomb.velocity.y -= 20 * multiplier;

        // Calculate new position
        state.bomb.x += state.bomb.velocity.x * multiplier;
        state.bomb.y += state.bomb.velocity.y * multiplier;

        // Rotate according to the direction
        const direction = state.currentPlayer === 1 ? -1 : +1;
        state.bomb.rotation += direction * 5 * multiplier;
      }

      function checkFrameHit() {
        // Stop throw animation once the bomb gets out of the left, bottom, or right edge of the screen
        if (
          state.bomb.y < 0 ||
          state.bomb.x < -state.shift / state.scale ||
          state.bomb.x > (window.innerWidth - state.shift) / state.scale
        ) {
          return true; // The bomb is off-screen
        }
      }

      function checkBuildingHit() {
        for (let i = 0; i < state.buildings.length; i++) {
          const building = state.buildings[i];
          if (
            state.bomb.x + 4 > building.x &&
            state.bomb.x - 4 < building.x + building.width &&
            state.bomb.y - 4 < 0 + building.height
          ) {
            // Check if the bomb is inside the blast hole of a previous impact
            for (let j = 0; j < state.blastHoles.length; j++) {
              const blastHole = state.blastHoles[j];

              // Check how far the bomb is from the center of a previous blast hole
              const horizontalDistance = state.bomb.x - blastHole.x;
              const verticalDistance = state.bomb.y - blastHole.y;
              const distance = Math.sqrt(
                horizontalDistance ** 2 + verticalDistance ** 2
              );
              if (distance < blastHoleRadius) {
                // The bomb is inside of the rectangle of a building,
                // but a previous bomb already blew off this part of the building
                return false;
              }
            }

            if (!simulationMode) {
              state.blastHoles.push({ x: state.bomb.x, y: state.bomb.y });
            }
            return true; // Building hit
          }
        }
      }

      function checkGorillaHit() {
        const enemyPlayer = state.currentPlayer === 1 ? 2 : 1;
        const enemyBuilding =
          enemyPlayer === 1
            ? state.buildings.at(1) // Second building
            : state.buildings.at(-2); // Second last building

        ctx.save();

        ctx.translate(
          enemyBuilding.x + enemyBuilding.width / 2,
          enemyBuilding.height
        );

        drawGorillaBody();
        let hit = ctx.isPointInPath(state.bomb.x, state.bomb.y);

        drawGorillaLeftArm(enemyPlayer);
        hit ||= ctx.isPointInStroke(state.bomb.x, state.bomb.y);

        drawGorillaRightArm(enemyPlayer);
        hit ||= ctx.isPointInStroke(state.bomb.x, state.bomb.y);

        ctx.restore();

        return hit;
      }

      function announceWinner() {
        if (settings.numberOfPlayers === 0) {
          winnerDOM.innerText = `Computer ${state.currentPlayer}`;
        } else if (
          settings.numberOfPlayers === 1 &&
          state.currentPlayer === 1
        ) {
          winnerDOM.innerText = `You`;
        } else if (
          settings.numberOfPlayers === 1 &&
          state.currentPlayer === 2
        ) {
          winnerDOM.innerText = `Computer`;
        } else {
          winnerDOM.innerText = `Player ${state.currentPlayer}`;
        }
        showCongratulations();
      }

      singlePlayerButtonDOM.forEach((button) =>
        button.addEventListener("click", () => {
          settings.numberOfPlayers = 1;
          gameModeDOM.innerHTML = "Player vs. Computer";
          name1DOM.innerText = "Player";
          name2DOM.innerText = "Computer";

          newGame();
        })
      );

      twoPlayersButtonDOM.forEach((button) =>
        button.addEventListener("click", () => {
          settings.numberOfPlayers = 2;
          gameModeDOM.innerHTML = "Player vs. Player";
          name1DOM.innerText = "Player 1";
          name2DOM.innerText = "Player 2";

          newGame();
        })
      );

      autoPlayButtonDOM.forEach((button) =>
        button.addEventListener("click", () => {
          settings.numberOfPlayers = 0;
          name1DOM.innerText = "Computer 1";
          name2DOM.innerText = "Computer 2";

          newGame();
        })
      );

      function generateWindSpeed() {
        // Generate a random number between -10 and +10
        return -10 + Math.random() * 20;
      }

      function setWindMillRotation() {
        const rotationSpeed = Math.abs(50 / state.windSpeed);
        windmillHeadDOM.style.animationDirection =
          state.windSpeed > 0 ? "normal" : "reverse";
        windmillHeadDOM.style.animationDuration = `${rotationSpeed}s`;

        windSpeedDOM.innerText = Math.round(state.windSpeed);
      }

      window.addEventListener("mousemove", function (e) {
        settingsDOM.style.opacity = 1;
        info1DOM.style.opacity = 1;
        info2DOM.style.opacity = 1;
      });

      const enterFullscreen = document.getElementById("enter-fullscreen");
      const exitFullscreen = document.getElementById("exit-fullscreen");

      function toggleFullscreen() {
        if (!document.fullscreenElement) {
          document.documentElement.requestFullscreen();
          enterFullscreen.setAttribute("stroke", "transparent");
          exitFullscreen.setAttribute("stroke", "white");
        } else {
          document.exitFullscreen();
          enterFullscreen.setAttribute("stroke", "white");
          exitFullscreen.setAttribute("stroke", "transparent");
        }
      }
    </script>
  </body>
</html>