O. Jueguito 3

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

<!DOCTYPE html>
<html>

<head>
 <meta charset="utf-8">
 <meta name="viewport"
   content="width=device-width">
 <title>Jueguito 3</title>
 <style>
  body {
   position: fixed;
   top: 0px;
   left: 0px;
   width: 100%;
   height: 100%;
   margin: 0;
   overflow: hidden;
  }

  .sprite {
   position: fixed;
  }
 </style>
 <script>
  //@ts-check
  /** @abstract */
  class Jugador
   extends HTMLElement {
   /** @param {number} velocidad */
   izquierda(velocidad) {
    throw new Error("abstract")
   }
   /** @param {number} velocidad */
   derecha(velocidad) {
    throw new Error("abstract")
   }
   /** @param {number} velocidad */
   sube(velocidad) {
    throw new Error("abstract")
   }
   /** @param {number} velocidad */
   baja(velocidad) {
    throw new Error("abstract")
   }
  }

  /** @abstract */
  class Figura
   extends HTMLElement {
   /**
    * @param {HTMLElement} jugador
    *   el jugador que es
    *   perseguido.
    */
   muevete(jugador) {
    throw new Error("abstract")
   }
  }

  /** @interface */
  class Fabrica {
   /**
    * Devuelve el único jugador,
    * por lo que se considera
    * Singleton.
    *  @returns {Jugador}
    */
   jugador() {
    throw new Error("interface")
   }
   /**
    * Devuelve un arreglo con las
    * figuras del juego.
    * @returns {Figura[]}
    */
   figuras() {
    throw new Error("interface")
   }
  }

  class Juego3 {
   /** @param {Fabrica} fabrica */
   constructor(fabrica) {
    this.jugador =
     fabrica.jugador()
    this.figuras =
     fabrica.figuras()
    this.iniciaX = null
    this.iniciaY = null
    this.interval = null
    this.activo = true
   }

   inicia() {
    document.addEventListener(
     "keydown",
     evt => this.teclas(evt))
    document.addEventListener(
     "touchstart",
     evt => this.iniciaTouch(evt))
    document.addEventListener(
     "touchmove",
     evt =>
      this.desplazaTouch(evt))
    this.interval = setInterval(
     () => this.mueveFiguras(), 60)
   }

   mueveFiguras() {
    for (const f of this.figuras) {
     f.muevete(this.jugador)
    }
    this.detectaColisiones()
   }

   detectaColisiones() {
    for (const f of this.figuras) {
     if (colisiona(
      this.jugador, f)) {
      this.termina()
      break
     }
    }
   }

   termina() {
    this.activo = false
    clearInterval(this.interval)
    this.jugador.innerHTML = "💥"
   }

   /** @param {KeyboardEvent} evt*/
   teclas(evt) {
    if (this.activo) {
     switch (evt.key) {
      case "ArrowLeft":
       this.jugador.izquierda(20)
       break
      case "ArrowRight":
       this.jugador.derecha(20)
       break
      case "ArrowUp":
       this.jugador.sube(20)
       break
      case "ArrowDown":
       this.jugador.baja(20)
       break
     }
     this.detectaColisiones()
    }
   }

   /** @param {TouchEvent} evt */
   iniciaTouch(evt) {
    if (this.activo) {
     const toqueInicial =
      evt.touches[0]
     this.iniciaX =
      toqueInicial.clientX
     this.iniciaY =
      toqueInicial.clientY
    }
   }

   /** @param {TouchEvent} evt */
   desplazaTouch(evt) {
    if (this.activo
     && this.iniciaX
     && this.iniciaY) {
     const desplazamiento =
      evt.touches[0]
     var desplazamientoX =
      desplazamiento.clientX
     var desplazamientoY =
      desplazamiento.clientY
     var difX = desplazamientoX -
      this.iniciaX
     var difY = desplazamientoY -
      this.iniciaY
     if (Math.abs(difX)
      + Math.abs(difY)
      > 150) {
      if (Math.abs(difX)
       > Math.abs(difY)) {
       if (difX > 70) {
        this.jugador.derecha(40)
       } else {
        this.jugador.izquierda(40)
       }
      } else if (difY > 70) {
       this.jugador.baja(40)
      } else {
       this.jugador.sube(40)
      }
      this.detectaColisiones()
      this.iniciaX = null
      this.iniciaY = null
     }
    }
   }
  }

  /**
   * @param {HTMLElement} e1
   * @param {HTMLElement} e2
   * @returns {boolean} true si los
   *    element colisionan.
   */
  function colisiona(e1, e2) {
   const rE1 =
    e1.getBoundingClientRect()
   const rE2 =
    e2.getBoundingClientRect()
   return (rE1.right >= rE2.left
    && rE1.left <= rE2.right
    && rE1.top <= rE2.bottom
    && rE1.bottom >= rE2.top)
  }

  customElements.define(
   "jugador-paloma",
   class extends Jugador {
    connectedCallback() {
     this.classList.add("sprite")
     this.innerHTML += "🕊"
     this.style.fontSize = "60px"
     const raiz =
      document.documentElement
     const r =
      this.getBoundingClientRect()
     const left =
      (raiz.clientWidth
       - r.width) /
      2
     const top =
      (raiz.clientHeight -
       r.height) /
      2
     this.style.left = `${left}px`
     this.style.top = `${top}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    sube(velocidad) {
     const top =
      this.getBoundingClientRect().
       top -
      velocidad
     this.style.top = `${top}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    baja(velocidad) {
     const top =
      this.getBoundingClientRect().
       top +
      velocidad
     this.style.top = `${top}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    izquierda(velocidad) {
     const left =
      this.getBoundingClientRect().
       left -
      velocidad
     this.style.left = `${left}px`
    }

    /**
     * @param {number} velocidad
     * @override
     */
    derecha(velocidad) {
     const left =
      this.getBoundingClientRect().
       left +
      velocidad
     this.style.left = `${left}px`
    }
   })

  customElements.define(
   "figura-aguila",
   class extends Figura {
    connectedCallback() {
     this.classList.add("sprite")
     this.innerHTML = "🦅"
     this.style.fontSize = "40px"
     const r =
      this.getBoundingClientRect()
     this.style.left =
      `${r.left}px`
     this.style.top = `${r.top}px`
     this.style.bottom = "auto"
     this.style.right = "auto"
    }

    /**
     * @param {HTMLElement} jugador
     *   el jugador que persigue.
     * @override
     */
    muevete(jugador) {
     const r =
      this.getBoundingClientRect()
     const rJ = jugador.
      getBoundingClientRect()
     const y2 = rJ.top
     const y1 = r.top
     const x2 = rJ.left
     const x1 = r.left
     const pendiente = x2 === x1 ?
      0 :
      (y2 - y1) / (x2 - x1)
     const dirección =
      x2 > x1 ? 1 : -1
     const x = x1 + dirección * 5
     const y =
      pendiente * (x - x1) + y1
     this.style.left =
      `${desvía(x)}px`
     this.style.top =
      `${desvía(y)}px`
    }
   })

  function desvía(i) {
   return i + 10 -
    20 * Math.random()
  }
 </script>
</head>

<body>
 <jugador-paloma></jugador-paloma>
 <figura-aguila
   style="right: 0; top: 0;">
 </figura-aguila>
 <figura-aguila
   style="right: 0; bottom: 0;">
 </figura-aguila>
 <script>
  //@ts-check
  /** @implements {Fabrica} */
  class MiFabrica {
   constructor() {
    /** @type {Jugador} */
    this.jugadorSingleton =
     document.querySelector(
      "jugador-paloma");
   }
   /**
    * Devuelve el único jugador,
    * por lo que se considera
    * Singleton.
    * @returns {Jugador}
    */
   jugador() {
    return this.jugadorSingleton
   }
   /**
    * Devuelve un arreglo con las
    * figuras del juego.
    * @returns {Figura[]}
    */
   figuras() {
    return Array.from(
     document.querySelectorAll(
      "figura-aguila"))
   }
  }
  const juego =
   new Juego3(new MiFabrica())
  juego.inicia()
 </script>
</body>

</html>