前端嘛 Logo
前端嘛
像素风解谜小游戏

像素风解谜小游戏

2026-02-02
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>[game] Copycat</title>
  <style>
    html,
    body {
      margin: 0;
      padding: 0;
      height: 100%;
    }

    body {
      color: white;
      background-color: #000;
      display: flex;
      align-items: center;
      justify-content: center;
      overflow: hidden;
    }

    canvas {
      flex-shrink: 0;
      background-color: #000;
      object-fit: contain;
    }

    .crisp {
      image-rendering: -moz-crisp-edges;
      image-rendering: -webkit-crisp-edges;
      image-rendering: pixelated;
    }

    /* 移动端控制按钮 */
    .mobile-controls {
      display: none;
      position: fixed;
      bottom: 20px;
      left: 0;
      right: 0;
      z-index: 1000;
      pointer-events: none;
      user-select: none;
      -webkit-user-select: none;
      touch-action: none;
    }

    @media (max-width: 768px),
    (pointer: coarse) {
      .mobile-controls {
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        padding: 0 20px 20px;
        gap: 20px;
      }
    }

    .control-group {
      display: flex;
      flex-direction: column;
      gap: 10px;
      pointer-events: auto;
    }

    .control-row {
      display: flex;
      gap: 10px;
      justify-content: center;
    }

    .control-btn {
      width: 60px;
      height: 60px;
      border-radius: 12px;
      background: rgba(255, 255, 255, 0.2);
      border: 2px solid rgba(255, 255, 255, 0.3);
      color: white;
      font-size: 24px;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
      transition: all 0.1s;
      user-select: none;
      -webkit-user-select: none;
    }

    .control-btn:active {
      background: rgba(255, 255, 255, 0.4);
      transform: scale(0.95);
    }

    .control-btn.direction {
      width: 50px;
      height: 50px;
    }

    .control-btn.action {
      background: rgba(76, 175, 80, 0.3);
      border-color: rgba(76, 175, 80, 0.5);
    }

    .control-btn.exit {
      background: rgba(244, 67, 54, 0.3);
      border-color: rgba(244, 67, 54, 0.5);
    }

    .control-btn.reset {
      background: rgba(255, 152, 0, 0.3);
      border-color: rgba(255, 152, 0, 0.5);
    }

    .dpad-container {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      grid-template-rows: repeat(3, 1fr);
      gap: 2px;
      width: 156px;
      height: 156px;
    }

    .dpad-btn {
      width: 50px;
      height: 50px;
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.2);
      border: 2px solid rgba(255, 255, 255, 0.3);
      color: white;
      font-size: 20px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
      transition: all 0.1s;
      user-select: none;
      -webkit-user-select: none;
    }

    .dpad-btn:active {
      background: rgba(255, 255, 255, 0.4);
      transform: scale(0.95);
    }

    .dpad-btn.up {
      grid-column: 2;
      grid-row: 1;
    }

    .dpad-btn.down {
      grid-column: 2;
      grid-row: 3;
    }

    .dpad-btn.left {
      grid-column: 1;
      grid-row: 2;
    }

    .dpad-btn.right {
      grid-column: 3;
      grid-row: 2;
    }
  </style>

</head>

<body>
  <!-- 移动端控制按钮 -->
  <div class="mobile-controls">
    <!-- 方向键控制 -->
    <div class="control-group">
      <div class="dpad-container">
        <button class="dpad-btn up" data-key="ArrowUp"></button>
        <button class="dpad-btn left" data-key="ArrowLeft"></button>
        <button class="dpad-btn right" data-key="ArrowRight"></button>
        <button class="dpad-btn down" data-key="ArrowDown"></button>
      </div>
    </div>

    <!-- 功能键控制 -->
    <div class="control-group">
      <button class="control-btn action" data-key="KeyX"></button>
      <button class="control-btn reset" data-key="KeyR">R</button>
      <button class="control-btn exit" data-key="KeyE"></button>
    </div>
  </div>

  <script>
    // ----------
    // Utility
    // ----------
    Util = {};
    Util.timeStamp = function () {
      return window.performance.now();
    };
    Util.random = function (min, max) {
      return min + Math.random() * (max - min);
    };
    Util.array2D = function (tableau, largeur) {
      var result = [];
      for (var i = 0; i < tableau.length; i += largeur)
        result.push(tableau.slice(i, i + largeur));
      return result;
    };
    Util.toDio = function (array) {
      let tab = array.map(x => {
        if (x !== 0) {
          return x - 1;
        } else {
          return x;
        }
      });
      let render = Util.array2D(tab, 16);
      return JSON.stringify(render);
    };
    Util.map = function (a, b, c, d, e) {
      return (a - b) / (c - b) * (e - d) + d;
    };
    Util.lerp = function (value1, value2, amount) {
      return value1 + (value2 - value1) * amount;
    };
    Util.linearTween = function (currentTime, start, degreeOfChange, duration) {
      return degreeOfChange * currentTime / duration + start;
    };
    Util.easeInOutQuad = function (t, b, c, d) {
      t /= d / 2;
      if (t < 1) return c / 2 * t * t + b;
      t--;
      return -c / 2 * (t * (t - 2) - 1) + b;
    };
    Util.easeInOutExpo = function (t, b, c, d) {
      t /= d / 2;
      if (t < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
      t--;
      return c / 2 * (-Math.pow(2, -10 * t) + 2) + b;
    };

    // ----------
    // Scene
    // ----------
    class Scene {
      constructor(name) {
        this.name = name;
        this.loop = true;
        this.init_once = false;
      }
      giveWorld(world) {
        this.world = world;
        this.ctx = world.ctx;
      }
      keyEvents(event) { }
      init() { }
      render() { }
      addEntity() { }
    }
    class Entity {
      constructor(scene, x, y) {
        this.scene = scene;
        this.world = scene.world;
        this.ctx = this.world.ctx;
        this.body = new Body(this, x, y);
      }
      setSprite(sprite_data) {
        this.sprite = new Sprite(this, sprite_data);
      }
      display() {
        if (this.sprite === undefined) {
          this.ctx.strokeStyle = "#000";
          this.ctx.strokeRect(
            this.body.position.x,
            this.body.position.y,
            this.body.size.x,
            this.body.size.y
          );
        } else {
          this.sprite.display();
        }
      }
      integration() {
        this.body.integration();
      }
    }

    // class for animated sprites !
    class Sprite {
      constructor(entity, sprite_data) {
        this.entity = entity;
        this.world = this.entity.world;
        this.tile_size = this.world.tile_size;
        this.ctx = this.world.ctx;
        // image data
        this.image = this.world.assets.image[sprite_data.image].image;
        // sprite
        this.size = sprite_data.size;
        this.current_frame = 0;
        this.animations = {};
        this.current_animation = undefined;
        this.width = this.image.width / this.size.x;
        this.height = this.image.height / this.size.y;
        // timer
        this.tick = 0;
        this.speed = 0.2;
        // offset
        this.offset = {
          x: 0,
          y: 0
        };
      }
      addAnimation(name, frames) {
        this.animations[name] = frames;
        this.current_animation = name;
      }
      animate(animation_name) {
        this.current_animation = animation_name;
        if (this.tick < 1) {
          this.tick += this.speed;
        } else {
          this.tick = 0;
          if (this.current_frame < this.animations[animation_name].length - 1) {
            this.current_frame += 1;
          } else {
            this.current_frame = 0;
          }
        }
      }
      display() {
        this.ctx.drawImage(
          this.image,
          Math.floor(
            this.animations[this.current_animation][this.current_frame] % this.width
          ) * this.size.x,
          Math.floor(
            this.animations[this.current_animation][this.current_frame] / this.width
          ) * this.size.y,
          this.size.x,
          this.size.y,
          this.entity.body.position.x +
          (this.tile_size / 2 - this.size.x / 2) +
          this.offset.x,
          this.entity.body.position.y +
          (this.tile_size / 2 - this.size.x / 2) +
          this.offset.y,
          this.size.x,
          this.size.y
        );
      }
    }

    class Body {
      constructor(entity, x, y) {
        this.world = entity.world;
        this.step = this.world.FPS.step;
        this.position = new Vector(x, y);
        this.next_position = new Vector(x, y);
        this.velocity = new Vector(0, 0);
        this.stepped_velocity = new Vector(0, 0);
        this.acceleration = new Vector(0, 0);
        this.drag = 0.98;
        this.size = {
          x: 16,
          y: 16
        };
      }
      setSize(x, y) {
        this.size.x = x;
        this.size.y = y;
      }
      updateVelocity() {
        this.velocity.add(this.acceleration);
        this.velocity.mult(this.drag);
        this.stepped_velocity = this.velocity.copy();
        this.stepped_velocity.mult(this.step);
        this.next_position = this.position.copy();
        this.next_position.add(this.stepped_velocity);
        // reset acceleration
        this.acceleration.mult(0);
      }
      updatePosition() {
        this.position.add(this.stepped_velocity);
      }
      integration() {
        this.updateVelocity();
        this.updatePosition();
      }
      applyForce(force_vector) {
        this.acceleration.add(force_vector);
      }
    }

    class Vector {
      constructor(x, y) {
        this.x = x || 0;
        this.y = y || 0;
      }
      set(x, y) {
        this.x = x;
        this.y = y;
      }
      add(vector) {
        this.x += vector.x;
        this.y += vector.y;
      }
      sub(vector) {
        this.x -= vector.x;
        this.y -= vector.y;
      }
      mult(scalar) {
        this.x *= scalar;
        this.y *= scalar;
      }
      div(scalar) {
        this.x /= scalar;
        this.y /= scalar;
      }
      limit(limit_value) {
        if (this.mag() > limit_value) this.setMag(limit_value);
      }
      mag() {
        return Math.hypot(this.x, this.y);
      }
      setMag(new_mag) {
        if (this.mag() > 0) {
          this.normalize();
        } else {
          this.x = 1;
          this.y = 0;
        }
        this.mult(new_mag);
      }
      dist(vector) {
        return new Vector(this.x - vector.x, this.y - vector.y).mag();
      }
      normalize() {
        let mag = this.mag();
        if (mag > 0) {
          this.x /= mag;
          this.y /= mag;
        }
      }
      heading() {
        return Math.atan2(this.x, this.y);
      }
      setHeading(angle) {
        let mag = this.mag();
        this.x = Math.cos(angle) * mag;
        this.y = Math.sin(angle) * mag;
      }
      copy() {
        return new Vector(this.x, this.y);
      }
    }

    class Box {
      constructor(world, box_data) {
        this.world = world;
        this.ctx = world.ctx;
        this.c_ctx = world.c_ctx;
        this.box_data = box_data;
        this.resolution = box_data.resolution;
        this.image = world.assets.image[box_data.image].image;
      }
      display(x, y, width, height) {
        // background
        this.ctx.fillRect(x + 1, y + 1, width - 2, height - 2);
        // corners
        this.ctx.lineWidth = 2;
        let coners = [0, 2, 6, 8];
        for (let i = 0; i < 4; i++) {
          let pos_x = x + Math.floor(i % 2) * (width - this.resolution),
            pos_y = y + Math.floor(i / 2) * (height - this.resolution);
          let clip_x = Math.floor(i % 2) * (this.resolution * 2),
            clip_y = Math.floor(i / 2) * (this.resolution * 2);
          this.ctx.drawImage(
            this.image,
            clip_x,
            clip_y,
            this.resolution,
            this.resolution,
            pos_x,
            pos_y,
            this.resolution,
            this.resolution
          );
        }
        let offset = this.resolution * 3;
        // top
        this.ctx.drawImage(
          this.image,
          8,
          0,
          this.resolution,
          this.resolution,
          x + 8,
          y,
          this.resolution + width - offset,
          this.resolution
        );
        // bottom
        this.ctx.drawImage(
          this.image,
          8,
          16,
          this.resolution,
          this.resolution,
          x + 8,
          y + height - this.resolution,
          this.resolution + width - offset,
          this.resolution
        );
        // left
        this.ctx.drawImage(
          this.image,
          0,
          8,
          this.resolution,
          this.resolution,
          x,
          y + 8,
          this.resolution,
          this.resolution + height - offset
        );
        // right
        this.ctx.drawImage(
          this.image,
          16,
          8,
          this.resolution,
          this.resolution,
          x + width - this.resolution,
          y + this.resolution,
          this.resolution,
          this.resolution + height - offset
        );
      }
    }
    // ----------
    // 🕹️ Diorama.js
    // ----------
    class Diorama {
      constructor(parameters) {
        this.parameters = parameters;
        // Game and author's name
        this.game_info = {
          name: parameters.name || "Untitled",
          author: parameters.author || "Anonymous"
        };
        // canvas
        this.background_color = parameters.background_color || "#000";
        this.initCanvas(parameters);
        // Assets
        this.counter = 0;
        this.toLoad = parameters.assets.length;
        this.assets = {
          image: {}
        };
        // keyboard event
        this.keys = {};
        // Scenes
        this.scenes = {};
        this.start_screen = parameters.start_screen || undefined;
        this.current_scene = "";
        // Bitmap font Data
        this.alphabet =
          "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ?!:',.()<>[]";
        this.fonts = {};
        // Maps
        this.tile_size = parameters.tile_size || 16;
        this.tiles_data = {};
        if (parameters.tiles !== undefined) {
          parameters.tiles.map(tile => {
            this.tiles_data[tile.id] = tile;
          });
        }
        this.mapsMax = parameters.maps.length;
        this.maps = {};
        if (parameters.maps !== undefined) {
          parameters.maps.map(map => {
            this.maps[map.name] = map;
          });
        }
        // Box system
        this.boxes = {};
        // By default the current font is the first font you create
        this.currentFont = undefined;
        // Game loop Data
        this.FPS = {
          now: 0,
          delta: 0,
          last: Util.timeStamp(),
          step: 1 / (parameters.frame_rate || 60)
        };
        this.requestChange = {
          value: false,
          action: ""
        };
        this.main_loop = undefined;
      }
      // ---
      // Setup & Loading
      // ---
      ready() {
        this.loadAssets(this.parameters.assets);
      }
      initCanvas(parameters) {
        this.canvas = document.createElement("canvas");
        this.ctx = this.canvas.getContext("2d");
        this.W = this.canvas.width = parameters.width || 256;
        this.H = this.canvas.height = parameters.height || 256;
        this.scale = parameters.scale || 1;
        this.full = false;
        this.ctx.imageSmoothingEnabled = false;
        this.canvas.classList.add("crisp");
        document.body.appendChild(this.canvas);
        // cache canvas
        this.cache = document.createElement("canvas");
        this.c_ctx = this.cache.getContext("2d");
      }
      loader() {
        // increment loader
        this.clear("#222");
        this.counter += 1;
        let padding = 20;
        let width = this.W - padding * 2,
          x = padding,
          y = this.H - padding * 2;
        this.ctx.fillStyle = "#111";
        this.ctx.fillRect(x, y, width, 20);
        this.ctx.fillStyle = "#333";
        this.ctx.fillRect(x, y, this.counter * width / this.toLoad, 20);
        this.ctx.strokeStyle = "#000";
        this.ctx.lineWidth = 4;
        this.ctx.strokeRect(x, y, width, 20);
        if (this.counter === this.toLoad) {
          this.launch();
        }
      }
      loadAssets(assets) {
        if (assets === undefined) console.log("Nothing to load");
        assets.map(obj => this.checkAssets(obj));
      }
      checkAssets(obj) {
        let subject = obj;
        switch (obj.type) {
          case "img":
            let img = new Image();
            img.onload = () => {
              this.loader();
            };
            img.onerror = () => {
              console.log("can't load Image: " + obj.name);
            };
            img.src = obj.path;
            subject.image = img;
            this.assets.image[obj.name] = subject;
            break;
          case undefined:
            console.log(obj.name, " doesn't have any type");
            break;
          default:
            console.log(obj.name, " has a none known type");
        }
      }
      launch() {
        this.eventSetup();
        this.initBoxes(this.parameters.boxes);
        this.initFonts(this.parameters.fonts);
        this.startScene(this.start_screen);
      }
      initBoxes(boxes_data) {
        if (boxes_data === undefined) return false;
        boxes_data.map(box => {
          this.boxes[box.name] = new Box(this, box);
        });
      }
      drawBox(box_name, x, y, width, height) {
        this.boxes[box_name].display(x, y, width, height);
      }
      // ---
      // Font manager
      // ---
      setFont(font_name) {
        this.currentFont = font_name;
      }
      initFonts(fonts_data) {
        if (fonts_data === undefined && fonts_data.length > 0) return false;
        fonts_data.map(font => {
          if (this.assets.image[font.image] === undefined) {
            console.log("can't load font, " + font.image + " doesn't exist");
            return false;
          }
          font.image = this.assets.image[font.image].image;
          this.fonts[font.name] = font;
        });
        // set current font to the first font !
        this.currentFont = Object.keys(this.fonts)[0];
      }
      write(text, x, y, justify, colorID) {
        if (this.currentFont === undefined) {
          console.log("No bitmap_font");
          return false;
        }
        if (typeof justify === "string") {
          switch (justify) {
            case "center":
              x -= text.length * this.fonts[this.currentFont].size.x / 2;
              break;
            case "right":
              x -= text.length * this.fonts[this.currentFont].size.x;
              break;
            default:
          }
          this.writeLine(text, x, y, colorID || 0);
        } else {
          this.writeParagraph(text, x, y, justify, colorID || 0);
        }
      }
      writeParagraph(text, x, y, justify, colorID) {
        let y_offset = 0,
          line_height = this.fonts[this.currentFont].size.y + 5,
          size_x = this.fonts[this.currentFont].size.x,
          words = text.split(" "),
          line = "";
        for (let i = 0; i < words.length; i++) {
          line += words[i] + " ";
          let nextword_width = 0,
            next_word = words[i + 1],
            line_length = line.length * size_x;
          next_word ? (nextword_width = next_word.length * size_x) : 0;
          if (line_length + nextword_width > justify) {
            this.writeLine(line, x, y + y_offset, 0, colorID);
            y_offset += line_height;
            line = "";
          } else {
            this.writeLine(line, x, y + y_offset, 0, colorID);
          }
        }
      }
      writeLine(text, x, y, colorID) {
        // write line
        let size_x = this.fonts[this.currentFont].size.x,
          size_y = this.fonts[this.currentFont].size.y,
          font_img = this.fonts[this.currentFont].image;
        for (let i = 0; i < text.length; i++) {
          let index = this.alphabet.indexOf(text.charAt(i)),
            clipX = size_x * index,
            posX = x + i * size_x;
          this.ctx.drawImage(
            font_img,
            clipX,
            colorID * size_y,
            size_x,
            size_y,
            posX,
            y,
            size_x,
            size_y
          );
        }
      }
      // -----------------
      // Events
      // -----------------
      eventSetup() {
        document.addEventListener("keydown", event => this.keyDown(event), false);
        document.addEventListener("keyup", event => this.keyUp(event), false);
      }
      keyDown(event) {
        event.preventDefault();
        this.keys[event.code] = true;
        if (this.keys.KeyF) {
          this.fullScreen();
        }
        if (this.keys.KeyM) {
          this.mute();
        }
        this.current_scene.keyEvents(event);
      }
      keyUp(event) {
        event.preventDefault();
        this.keys[event.code] = false;
      }
      // ---
      // Scene Manager
      // ---
      startScene(scene_name) {
        // check if the scene exist
        if (this.scenes[scene_name] === undefined)
          return scene_name + " - doesn't exist";
        // request the change of scene if this.main_loop is active
        if (this.main_loop !== undefined) {
          this.requestChange.value = true;
          this.requestChange.action = scene_name;
          return false;
        }
        this.requestChange.value = false;
        this.requestChange.action = "";
        this.FPS.last = Util.timeStamp();
        this.current_scene = this.scenes[scene_name];
        this.initScene();
        // does this scenes needs a gameloop ?
        if (this.current_scene.loop === true) {
          this.gameLoop();
        } else {
          this.mainRender();
        }
      }
      initScene() {
        if (this.current_scene.init_once) return false;
        this.current_scene.init();
      }
      addScene(scene) {
        // links this world to this scene
        scene.giveWorld(this);
        this.scenes[scene.name] = scene;
      }
      // ---
      // Main Loop
      // ---
      mainRender() {
        this.clear();
        this.current_scene.render();
      }
      loopCheck() {
        if (this.requestChange.value === false) {
          this.main_loop = requestAnimationFrame(() => this.gameLoop());
        } else {
          cancelAnimationFrame(this.main_loop);
          this.main_loop = undefined;
          this.startScene(this.requestChange.action);
        }
      }
      gameLoop() {
        this.FPS.now = Util.timeStamp();
        this.FPS.delta += Math.min(1, (this.FPS.now - this.FPS.last) / 1000);
        while (this.FPS.delta > this.FPS.step) {
          this.FPS.delta -= this.FPS.step;
          this.mainRender();
        }
        this.FPS.last = this.FPS.now;
        this.loopCheck();
      }
      // Basic functions
      clear(custom_color) {
        this.ctx.fillStyle = custom_color || this.background_color;
        this.ctx.fillRect(0, 0, this.W, this.H);
      }
      setScale() {
        this.canvas.style.width = this.W * this.scale + "px";
        this.canvas.style.height = this.H * this.scale + "px";
      }
      fullScreen() {
        this.full = !this.full;
        if (!this.full) {
          this.setScale();
        } else {
          this.canvas.style.width = "100%";
          this.canvas.style.height = "100%";
        }
      }
      // ---
      // Tile map
      // ---
      getTile(layer_id, x, y) {
        if (x < 0 || x > this.terrain.layers[layer_id].size.x - 1) return false;
        if (y < 0 || y > this.terrain.layers[layer_id].size.y - 1) return false;
        let tile = this.tiles_data[this.terrain.layers[layer_id].geometry[y][x]];
        if (tile === undefined) return false;
        return tile;
      }
      findTile(layer_id, tile_id) {
        let layer = this.terrain.layers[layer_id];
        let result = [];
        for (let y = 0; y < layer.size.y; y++) {
          for (let x = 0; x < layer.size.x; x++) {
            let id = layer.geometry[y][x];
            if (id === tile_id) {
              result.push({ x: x, y: y });
            }
          }
        }
        return result;
      }
      initMap(map_name) {
        this.terrain = JSON.parse(JSON.stringify(this.maps[map_name]));
        // give size to layers
        for (var i = 0; i < this.terrain.layers.length; i++) {
          this.terrain.layers[i].size = {
            x: this.terrain.layers[i].geometry[0].length,
            y: this.terrain.layers[i].geometry.length
          };
        }
        this.terrain.tileset = this.assets.image[this.maps[map_name].tileset].image;
        this.terrain.tileset_size = {
          width: this.terrain.tileset.width / this.tile_size,
          height: this.terrain.tileset.height / this.tile_size + 1
        };
        this.terrain.layers.forEach((layer, index) => {
          this.marchingSquare(layer);
          this.bitMasking(layer);

          // create a cache for reducing draw call in the gameLoop
          this.terrainCache(layer);
          // prepare animated tiles
          layer.animated = [];
          for (var id in this.tiles_data) {
            if (this.tiles_data[id].animated === true) {
              let tiles = this.findTile(index, parseInt(id));
              layer.animated.push({
                id: id,
                spritesheet: this.assets.image[this.tiles_data[id].spritesheet]
                  .image,
                positions: tiles,
                current: 0,
                steps: this.tiles_data[id].steps,
                max_frame:
                  this.assets.image[this.tiles_data[id].spritesheet].image.width /
                  this.tile_size
              });
            }
          }
        });
        this.clear("black");
      }
      terrainCache(layer) {
        layer.cache = {};
        let c = (layer.cache.c = document.createElement("canvas"));
        let ctx = (layer.cache.ctx = layer.cache.c.getContext("2d"));
        let W = (c.width = layer.size.x * this.tile_size),
          H = (c.height = layer.size.y * this.tile_size);
        // Draw on cache
        this.ctx.clearRect(0, 0, W, H);
        this.drawLayer(layer);
        ctx.drawImage(this.canvas, 0, 0);
        this.clear();
      }
      marchingSquare(layer) {
        layer.square = [];
        for (let y = 0; y < layer.size.y; y++) {
          for (let x = 0; x < layer.size.x; x++) {
            let p1 = 0,
              p2 = 0,
              p3 = 0,
              p4 = 0;

            if (y + 1 < layer.size.y && x + 1 < layer.size.x) {
              p1 = layer.geometry[y][x];
              p2 = layer.geometry[y][x + 1];
              p3 = layer.geometry[y + 1][x + 1];
              p4 = layer.geometry[y + 1][x];
            }
            let id = p1 * 8 + p2 * 4 + p3 * 2 + p4;
            layer.square.push(id);
          }
        }

        layer.square = Util.array2D(layer.square, layer.size.x);
      }
      bitMasking(layer) {
        layer.bitMask = [];
        for (let y = 0; y < layer.size.y; y++) {
          for (let x = 0; x < layer.size.x; x++) {
            let id = layer.geometry[y][x];
            let neighbor = [0, 0, 0, 0];
            if (y - 1 > -1) {
              if (id === layer.geometry[y - 1][x]) {
                //top
                neighbor[0] = 1;
              }
            } else {
              neighbor[0] = 1;
            }
            if (x - 1 > -1) {
              if (id === layer.geometry[y][x - 1]) {
                // left
                neighbor[1] = 1;
              }
            } else {
              neighbor[1] = 1;
            }
            if (x + 1 < layer.size.x) {
              if (id === layer.geometry[y][x + 1]) {
                // right
                neighbor[2] = 1;
              }
            } else {
              neighbor[2] = 1;
            }

            if (y + 1 < layer.size.y) {
              if (id === layer.geometry[y + 1][x]) {
                //down
                neighbor[3] = 1;
              }
            } else {
              neighbor[3] = 1;
            }
            id =
              1 * neighbor[0] + 2 * neighbor[1] + 4 * neighbor[2] + 8 * neighbor[3];
            layer.bitMask.push(id);
          }
        }
        layer.bitMask = Util.array2D(layer.bitMask, layer.size.x);
      }
      renderMap() {
        this.terrain.layers.forEach(layer => {
          this.ctx.drawImage(layer.cache.c, 0, 0);
          // draw animated layer
          layer.animated.forEach(tile => {
            if (tile.current < tile.max_frame - 1) {
              tile.current += tile.steps;
            } else {
              tile.current = 0;
            }
            // render animated tiles
            tile.positions.forEach(position => {
              let x = position.x * this.tile_size,
                y = position.y * this.tile_size;
              this.ctx.drawImage(
                tile.spritesheet,
                Math.floor(tile.current) * this.tile_size,
                0,
                this.tile_size,
                this.tile_size,
                x,
                y,
                this.tile_size,
                this.tile_size
              );
            });
          });
        });
      }
      drawMap() {
        this.terrain.layers.forEach(layer => {
          this.drawLayer(layer);
        });
      }
      drawLayer(layer) {
        for (let y = 0; y < layer.size.y; y++) {
          for (let x = 0; x < layer.size.x; x++) {
            // ID of the tile
            let id = layer.geometry[y][x];
            // Don't draw invisible tiles
            // Position of the tile :)
            let positionX = x * this.tile_size + layer.offset.x,
              positionY = y * this.tile_size + layer.offset.y;
            let sourceX =
              Math.floor(id % this.terrain.tileset_size.width) * this.tile_size,
              sourceY =
                Math.floor(id / this.terrain.tileset_size.width) * this.tile_size;
            if (this.tiles_data[id] && this.tiles_data[id].look === "bitmask") {
              sourceX = Math.floor(layer.bitMask[y][x]) * this.tile_size;
              sourceY = this.tiles_data[id].line * this.tile_size;
            }

            if (layer.look === "square") {
              if (layer.square[y][x] === 0) continue;
              positionX += this.tile_size / 2;
              positionY += this.tile_size / 2;
              sourceX = Math.floor(layer.square[y][x] % 16) * 16;
              sourceY = 7 * this.tile_size;
            }

            if (this.tiles_data[id] && this.tiles_data[id].animated === true) {
              // hide animated sprites on the cache
              continue;
            }

            // render tile

            this.ctx.drawImage(
              this.terrain.tileset,
              sourceX,
              sourceY,
              this.tile_size,
              this.tile_size,
              positionX,
              positionY,
              this.tile_size,
              this.tile_size
            );
          }
        }
      }
    }
    let parameters = {
      name: "Copycat",
      start_screen: "menu",
      background_color: "#223d8c",
      width: 256,
      height: 256,
      tile_size: 16,
      assets: [
        // Images
        {
          type: "img",
          name: "coderscrux_font",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/coderscrux_font.png"
        },
        {
          type: "img",
          name: "controls",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/controls.png"
        },
        {
          type: "img",
          name: "player_sprite",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/player_sprite.png"
        },
        {
          type: "img",
          name: "spawn_effect",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/spawn_effect.png"
        },
        {
          type: "img",
          name: "water_splash",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/water_splash.png"
        },
        {
          type: "img",
          name: "shadow",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/shadow.png"
        },
        {
          type: "img",
          name: "main_title",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/main_title.png"
        },
        {
          type: "img",
          name: "origami_dark",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/origami_dark.png"
        },
        {
          type: "img",
          name: "origami_light",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/origami_light.png"
        },
        {
          type: "img",
          name: "box_texture",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/box_texture.png"
        },
        {
          type: "img",
          name: "selection",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/selection.png"
        },
        {
          type: "img",
          name: "flat_frame",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/flat_frame.png"
        },
        {
          type: "img",
          name: "pattern",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/pattern.png"
        },
        {
          type: "img",
          name: "cursor",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/cursor.png"
        },
        {
          type: "img",
          name: "demo_tileset",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/demo_tileset.png"
        },
        {
          type: "img",
          name: "exit",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/exit.png"
        },
        {
          type: "img",
          name: "water_sprite",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/water_sprite.png"
        },
        {
          type: "img",
          name: "dust_effect",
          path: "https://fecoder-1302080640.cos.ap-nanjing.myqcloud.com/copycat/dust_effect.png"
        }

        // Bitmap font
      ],
      fonts: [
        // basic font
        {
          name: "coderscrux",
          image: "coderscrux_font",
          size: { x: 6, y: 9 }
        },
        {
          name: "origami_dark",
          image: "origami_dark",
          size: { x: 8, y: 9 }
        },
        {
          name: "origami_light",
          image: "origami_light",
          size: { x: 8, y: 9 }
        }
      ],
      // box system
      boxes: [
        {
          name: "box",
          resolution: 8,
          image: "box_texture"
        },
        {
          name: "selection",
          resolution: 8,
          image: "selection"
        },
        {
          name: "flat_frame",
          resolution: 8,
          image: "flat_frame"
        }
      ],
      tiles: [
        { name: "empty", id: 0, collision: false, visibility: false },
        { name: "water", id: 1, collision: false, look: "square", line: 7 },
        { name: "shores", id: 2, collision: false, look: "bitmask", line: 6 },
        { name: "ground", id: 3, collision: false, look: "bitmask", line: 1 },
        { name: "wall", id: 4, collision: true, look: "bitmask", line: 2 },
        { name: "fence", id: 11, collision: true, look: "bitmask", line: 4 },
        { name: "bush", id: 5, collision: true },
        { name: "ice", id: 6, collision: false, look: "bitmask", line: 3 },
        { name: "spawn", id: 7, collision: false },
        {
          name: "exit",
          id: 8,
          collision: false,
          animated: true,
          spritesheet: "exit",
          steps: 0.4
        },
        {
          name: "waves",
          id: 16,
          collision: false,
          animated: true,
          spritesheet: "water_sprite",
          steps: 0.2
        },
        { name: "trap", id: 9, collision: false },
        { name: "hole", id: 10, collision: true },
        // arrows
        { name: "arrowLeft", id: 12, collision: false },
        { name: "arrowUp", id: 13, collision: false },
        { name: "arrowRight", id: 14, collision: false },
        { name: "arrowDown", id: 15, collision: false }
      ],
      maps: [
        // map 1
        {
          name: "map_1",
          tileset: "demo_tileset",
          // ground
          layers: [
            // ground layer
            {
              name: "ground",
              offset: {
                x: 0,
                y: 4
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            },
            // ice / arrows / layer
            {
              name: "onGround",
              offset: {
                x: 0,
                y: 0
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            }
            // wall layer
          ]
        },
        // map 2
        {
          name: "map_2",
          tileset: "demo_tileset",
          // ground
          layers: [
            // ground layer
            {
              name: "ground",
              offset: {
                x: 0,
                y: 4
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            },
            // ice / arrows / layer
            {
              name: "onGround",
              offset: {
                x: 0,
                y: 0
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 11, 11, 11, 11, 11, 0, 0, 11, 11, 11, 11, 11, 0, 0],
                [0, 0, 11, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 11, 0, 0],
                [0, 0, 11, 0, 8, 0, 11, 0, 0, 11, 0, 8, 0, 11, 0, 0],
                [0, 0, 11, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 11, 0, 0],
                [0, 0, 11, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 11, 0, 0],
                [0, 0, 11, 4, 4, 0, 11, 0, 0, 11, 0, 0, 0, 11, 0, 0],
                [0, 0, 11, 0, 0, 0, 11, 0, 0, 11, 0, 4, 4, 11, 0, 0],
                [0, 0, 11, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 11, 0, 0],
                [0, 0, 11, 0, 7, 0, 11, 0, 0, 11, 0, 7, 0, 11, 0, 0],
                [0, 0, 11, 11, 11, 11, 11, 0, 0, 11, 11, 11, 11, 11, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            }
            // wall layer
          ]
        },
        {
          name: "map_3",
          tileset: "demo_tileset",
          // ground
          layers: [
            // ground layer
            {
              name: "ground",
              offset: {
                x: 0,
                y: 4
              },
              geometry: [
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
              ]
            },
            // ice / arrows / layer
            {
              name: "onGround",
              offset: {
                x: 0,
                y: 0
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 4, 4, 4, 4, 4, 0, 0, 4, 4, 4, 4, 4, 0, 0],
                [0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
                [0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
                [0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 4, 0, 0],
                [0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
                [0, 0, 4, 8, 0, 0, 0, 0, 0, 0, 5, 0, 0, 4, 0, 0],
                [0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
                [0, 0, 4, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0],
                [0, 0, 4, 7, 0, 0, 0, 0, 0, 0, 0, 7, 0, 4, 0, 0],
                [0, 0, 4, 4, 4, 4, 4, 0, 0, 4, 4, 4, 4, 4, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            }
            // wall layer
          ]
        },

        {
          name: "map_4",
          tileset: "demo_tileset",
          // ground
          layers: [
            // ground layer
            {
              name: "ground",
              offset: {
                x: 0,
                y: 4
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0],
                [0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0],
                [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0],
                [0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            },
            // ice / arrows / layer
            {
              name: "onGround",
              offset: {
                x: 0,
                y: 0
              },
              geometry: [
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 11, 0, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 11, 0, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 11, 9, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 8, 11, 0, 0, 0, 11, 8, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 11, 7, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 0, 11, 0, 0, 0, 11, 0, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 0, 11, 5, 0, 0, 11, 11, 11, 0, 0, 0],
                [0, 0, 0, 0, 11, 7, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
              ]
            } // wall layer
          ]
        },
        {
          name: "map_5",
          tileset: "demo_tileset",
          // ground
          layers: [
            // ground layer
            {
              name: "ground",
              offset: {
                x: 0,
                y: 4
              },
              geometry: [
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
              ]
            },
            // ice / arrows / layer
            {
              name: "onGround",
              offset: {
                x: 0,
                y: 0
              },
              geometry: [
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 0, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 8, 4, 4],
                [4, 4, 4, 6, 6, 4, 6, 6, 4, 6, 4, 4, 6, 4, 4, 4],
                [4, 4, 4, 4, 6, 6, 6, 6, 4, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 5, 6, 6, 6, 6, 0, 0, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 0, 7, 4, 4, 4],
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 4],
                [4, 5, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 4],
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
              ]
            } // wall layer
          ]
        },

        {
          name: "map_6",
          tileset: "demo_tileset",
          // ground
          layers: [
            // ground layer
            {
              name: "ground",
              offset: {
                x: 0,
                y: 4
              },
              geometry: [
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
                [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
              ]
            },
            // ice / arrows / layer
            {
              name: "onGround",
              offset: {
                x: 0,
                y: 0
              },
              geometry: [
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 4, 4, 6, 6, 14, 0, 6, 6, 6, 6, 15, 4, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 8, 6, 6, 6, 4, 6, 6, 4, 4, 4],
                [4, 4, 4, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 7, 6, 4, 4, 4],
                [4, 4, 4, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 4, 4, 4],
                [4, 4, 4, 6, 6, 14, 6, 6, 6, 6, 6, 6, 4, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 0, 6, 4, 4, 4, 4],
                [4, 4, 4, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 8, 13, 4, 4, 4],
                [4, 4, 4, 6, 6, 6, 7, 6, 6, 6, 6, 6, 6, 4, 4, 4],
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
                [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
              ]
            } // wall layer
          ]
        }
      ]
    };

    // Don't mind me
    // just too lazy to modify the maps by hand
    parameters.maps.forEach(map => {
      new_layer = {};
      new_layer.name = "water";
      new_layer.look = "square";
      new_layer.offset = { x: 0, y: 8 };
      new_layer.geometry = Array(16)
        .fill()
        .map(() => Array(16).fill(0));
      map.layers.unshift(new_layer);
      //
      new_layer = {};
      new_layer.name = "splash";
      new_layer.offset = { x: 0, y: 8 };
      new_layer.geometry = Array(16)
        .fill()
        .map(() => Array(16).fill(0));
      map.layers.splice(2, 0, new_layer);

      let water = map.layers[0];
      let ground = map.layers[1];
      let splash = map.layers[2];

      for (let y = 0; y < ground.geometry.length; y++) {
        for (let x = 0; x < ground.geometry[0].length; x++) {
          if (
            y - 1 > 0 &&
            ground.geometry[y][x] !== 3 &&
            ground.geometry[y - 1][x] == 3
          ) {
            ground.geometry[y][x] = 2;
          }
        }
      }

      for (let y = 0; y < ground.geometry.length; y++) {
        for (let x = 0; x < ground.geometry[0].length; x++) {
          if (ground.geometry[y][x] == 2) {
            splash.geometry[y][x] = 16;
          }
        }
      }

      for (let y = 0; y < water.geometry.length; y++) {
        for (let x = 0; x < water.geometry[0].length; x++) {
          if (ground.geometry[y][x] == 3) {
            water.geometry[y][x] = 1;
          }

          if (ground.geometry[y][x] !== 3 && ground.geometry[y][x + 1] == 3) {
            water.geometry[y][x] = 1;
          }
          if (ground.geometry[y][x] !== 3 && ground.geometry[y][x - 1] == 3) {
            water.geometry[y][x] = 1;
          }
          if (
            y + 1 < water.geometry.length &&
            ground.geometry[y][x] !== 3 &&
            ground.geometry[y + 1][x] == 3
          ) {
            water.geometry[y][x] = 1;
          }
          if (
            y - 1 > 0 &&
            ground.geometry[y][x] !== 3 &&
            ground.geometry[y - 1][x] == 3
          ) {
            water.geometry[y][x] = 1;
          }
        }
      }

      for (let y = 0; y < water.geometry.length; y++) {
        for (let x = 0; x < water.geometry[0].length; x++) {
          if (water.geometry[y][x] == -1) {
            water.geometry[y][x] = 1;
          }
        }
      }
    });
    // menu scene
    let menu = new Scene("menu");
    menu.keyEvents = function (event) {
      if (this.world.keys.ArrowDown && this.selection < this.button.length - 1) {
        this.selection += 1;
      } else if (this.world.keys.ArrowUp && this.selection > 0) {
        this.selection -= 1;
      }
      if (this.world.keys.KeyX) {
        this.world.startScene(this.button[this.selection].link);
      }
    };
    menu.init = function () {
      this.init_once = true;
      // custom data
      this.button = [
        {
          name: "PLAY",
          link: "inGame"
        },
        {
          name: "SELECT",
          link: "levels"
        },
        {
          name: "CONTROLS",
          link: "controls"
        }
      ];
      this.texteMax =
        Math.max(...this.button.map(button => button.name.length)) * 6;
      this.selection = 0;
      this.select_pos = {
        x: this.world.W / 2,
        y: 110
      };
      this.cursor_phase = 0;
      this.cursor = this.world.assets.image.cursor.image;
      // background
      let background_image = this.world.assets.image.pattern.image;
      this.pattern = this.world.ctx.createPattern(background_image, "repeat");
      this.offset = {
        x: 0,
        y: 0
      };
      // add cat on
      this.cat = new Entity(this, -this.world.tile_size, -this.world.tile_size);
      let sprite_data = {
        image: "player_sprite",
        size: {
          x: 18,
          y: 18
        }
      };
      this.cat.setSprite(sprite_data);
      this.cat.sprite.addAnimation("idle", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
      this.cat.sprite.speed = 0.2;
      this.cat.sprite.offset.y = -3;
    };
    menu.render = function () {
      this.animatedBackground();
      this.ctx.drawImage(this.world.assets.image["main_title"].image, 0, 0);
      this.displaySelection();
      // notice
      this.world.ctx.fillStyle = "rgba(0,0,0,0.6)";
      this.world.ctx.fillRect(0, this.world.H - 50, this.world.W, 33);
      this.world.setFont("origami_light");
      this.world.write(
        "Arrow keys to select",
        this.world.W / 2,
        this.world.H - 46,
        "center"
      );
      this.world.write(
        "[x] to Confirm",
        this.world.W / 2,
        this.world.H - 30,
        "center"
      );
    };
    menu.displaySelection = function () {
      // display box
      this.ctx.fillStyle = "#82769e";
      this.world.drawBox(
        "box",
        this.select_pos.x - (this.texteMax + 60) / 2,
        this.select_pos.y - 16,
        this.texteMax + 60,
        this.button.length * 20 + 20
      );
      // display text and cursor
      for (i in this.button) {
        if (i == this.selection) {
          this.world.setFont("origami_light");
        } else {
          this.world.setFont("origami_dark");
        }
        let title = this.button[i].name;
        this.world.write(
          title,
          this.select_pos.x,
          this.select_pos.y + i * 20,
          "center"
        );
      }
      this.cursor_phase += 0.1;
      if (this.cursor_phase > 1 / Math.sin(0.2)) {
        this.cursor_phase = -1;
      }
      let x = this.select_pos.x + Math.sin(this.cursor_phase) * 2 - 20;
      this.world.ctx.drawImage(
        this.cursor,
        x - this.button[this.selection].name.length * 10 / 2,
        this.select_pos.y + 20 * this.selection - 2
      );
    };
    menu.animatedBackground = function () {
      this.offset.x += 0.8;
      this.offset.y += 0.6;
      if (this.offset.x > 63) {
        this.offset.x = 0;
      }
      if (this.offset.y > 63) {
        this.offset.y = 0;
      }
      let ctx = this.world.ctx;
      ctx.save();
      ctx.translate(this.offset.x, this.offset.y);
      ctx.fillStyle = this.pattern;
      ctx.fillRect(-this.offset.x, -this.offset.y, this.world.W, this.world.H);
      ctx.restore();
    };
    let levels = new Scene("levels");
    levels.keyEvents = function (event) {
      if (this.world.keys.KeyE) {
        this.world.startScene("menu");
      }
      if (this.world.keys.ArrowDown && this.selection + 5 < this.world.mapsMax) {
        this.selection += 5;
      }
      if (this.world.keys.ArrowUp && this.selection - 5 >= 0) {
        this.selection -= 5;
      }
      if (this.world.keys.ArrowRight && this.selection + 1 < this.world.mapsMax) {
        this.selection += 1;
      }
      if (this.world.keys.ArrowLeft && this.selection - 1 >= 0) {
        this.selection -= 1;
      }
      if (this.world.keys.KeyX) {
        this.world.current_level = this.selection + 1;
        this.world.startScene("inGame");
      }
    };
    levels.init = function () {
      this.init_once = true;
      this.selection = 0;
      this.scale = 0;
    };
    levels.render = function () {
      this.world.clear("black");
      // animate selection
      this.scale += 0.1;
      if (this.scale > 1 / Math.sin(0.2)) {
        this.scale = -1;
      }
      let offset = Math.sin(this.scale) * 2;
      // display box
      this.ctx.fillStyle = "#82769e";
      this.world.drawBox("box", 16, 16, this.world.W - 32, this.world.H - 46 - 32);
      this.world.setFont("origami_light");
      this.world.setFont("origami_dark");
      let show = Math.min(this.world.mapsMax, 20);
      for (let i = 0; i < show; i++) {
        let level_id = i + 20 * Math.floor(this.selection / 20);
        let position_x = 32 + Math.floor(i % 5) * 40,
          position_y = 32 + Math.floor(i / 5) * 40;
        if (level_id == this.selection) {
          this.world.setFont("origami_light");
          this.world.drawBox(
            "selection",
            position_x - offset / 2,
            position_y - offset / 2,
            24 + offset,
            24 + offset
          );
        } else {
          this.world.setFont("origami_dark");
          this.world.drawBox("flat_frame", position_x, position_y, 24, 24);
        }
        this.world.write(
          (level_id + 1).toString(),
          position_x + 13,
          position_y + 8,
          "center"
        );
      }
      // notice
      this.world.ctx.fillStyle = "rgba(0,0,0,0.6)";
      this.world.ctx.fillRect(0, this.world.H - 50, this.world.W, 33);
      this.world.setFont("origami_light");
      this.world.write(
        "Arrow keys to select",
        this.world.W / 2,
        this.world.H - 46,
        "center"
      );
      this.world.write(
        "[x] to Confirm, [E] to exit",
        this.world.W / 2,
        this.world.H - 30,
        "center"
      );
    };
    let inGame = new Scene("inGame");
    inGame.keyEvents = function (event) {
      if (this.world.keys.KeyE && this.userInput) {
        this.transition.start(
          0,
          Math.max(this.world.W / 2, this.world.H / 2),
          () => {
            this.world.startScene("menu");
          }
        );
      }
      if (this.world.keys.KeyR && this.userInput) {
        this.transition.start(
          0,
          Math.max(this.world.W / 2, this.world.H / 2),
          () => {
            this.world.startScene("inGame");
          }
        );
      }
    };
    inGame.init = function () {
      this.won = false;
      this.userInput = true;
      this.world.initMap("map_" + this.world.current_level);
      this.cats = [];
      let spawn_cat = () => {
        // add cats on spawn tile_size
        let spawns = this.world.findTile(3, 7);
        spawns.forEach(spawn => {
          this.addCat(spawn.x, spawn.y);
        });
      };
      // effects
      this.effects = [];
      // transition effects
      this.transition = {
        scene: this,
        active: true,
        // between 0 and 100
        state: 0,
        value: 0,
        duration: 500,
        start: 0,
        // between whatever and whatever
        from: 0,
        to: Math.max(this.world.W, this.world.H),
        //
        start: function (from, to, callback) {
          this.scene.userInput = false;
          this.active = true;
          this.from = from;
          this.start_time = new Date();
          this.to = to;
          this.callback = callback;
        },
        update: function () {
          let time = new Date() - this.start_time;
          if (time < this.duration) {
            this.value = Util.easeInOutQuad(
              time,
              this.from,
              this.to - this.from,
              this.duration
            );
          } else {
            this.active = false;
            this.scene.userInput = true;
            if (this.callback !== undefined) {
              this.callback();
            }
          }
        },
        render: function () {
          this.scene.ctx.fillStyle = "black";
          this.scene.ctx.fillRect(0, 0, this.scene.world.W, this.value);
          this.scene.ctx.fillRect(
            0,
            this.scene.world.H,
            this.scene.world.W,
            -this.value
          );
          this.scene.ctx.fillRect(0, 0, this.value, this.scene.world.H);
          this.scene.ctx.fillRect(
            this.scene.world.W,
            0,
            -this.value,
            this.scene.world.H
          );
        }
      };
      this.transition.start(
        Math.max(this.world.W / 2, this.world.H / 2),
        0,
        spawn_cat
      );
    };
    inGame.addCat = function (x, y) {
      let cat = new Cat(this, x, y);
      let sprite_data = {
        image: "player_sprite",
        size: {
          x: 18,
          y: 18
        }
      };
      cat.setSprite(sprite_data);
      cat.sprite.addAnimation("idle", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
      cat.sprite.speed = 0.2;
      cat.sprite.offset.y = -3;
      let spawn_data = {
        image: "spawn_effect",
        frames: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        size: {
          x: 20,
          y: 40
        }
      };
      let spawn_effect = new Effect(this, spawn_data, x, y - 1, () => {
        this.cats.push(cat);
      });
      spawn_effect.trigger = 4;
      this.effects.push(spawn_effect);
    };
    inGame.render = function () {
      this.control();
      this.world.renderMap();
      for (let i = this.cats.length; i--;) {
        this.cats[i].sprite.animate("idle");
        // draw shadow and cat
        this.ctx.drawImage(
          this.world.assets.image["shadow"].image,
          this.cats[i].body.position.x,
          this.cats[i].body.position.y + 2
        );
        this.cats[i].display();
        this.cats[i].translation();
      }
      for (let i = this.effects.length; i--;) {
        this.effects[i].render();
      }
      if (this.transition.active) {
        this.transition.update();
        this.transition.render();
      }
    };
    inGame.control = function () {
      if (this.userInput == false) return false;
      if (this.world.keys.ArrowUp) {
        this.moveCats(0, -1);
      }
      if (this.world.keys.ArrowDown) {
        this.moveCats(0, 1);
      }
      if (this.world.keys.ArrowLeft) {
        this.moveCats(-1, 0);
      }
      if (this.world.keys.ArrowRight) {
        this.moveCats(1, 0);
      }
    };
    inGame.moveCats = function (x, y) {
      // see if every cat are ready to move
      let canMove = this.cats.every(cat => {
        return cat.inTranslation == false;
      });
      if (!canMove) return false;
      this.cats.forEach(cat => {
        if (cat.canBeControlled === false) return false;
        if (cat.isDead) return false;
        cat.move(x, y);
      });
      this.collisionCats();
      this.cats.forEach(cat => {
        cat.applyMove();
      });
    };
    inGame.collisionCats = function () {
      // check for other cats !
      let need_to_check = true;
      while (need_to_check === true) {
        need_to_check = false;
        this.cats.forEach(cat => {
          if (cat.checkOthers()) {
            cat.target = cat.old_position.copy();
            need_to_check = true;
          }
        });
      }
    };
    inGame.checkWin = function () {
      if (this.cats.length === 0) {
        // everyone is dead :/
        this.transition.start(
          0,
          Math.max(this.world.W / 2, this.world.H / 2),
          () => {
            this.world.startScene("inGame");
          }
        );
        return false;
      }
      let win = this.cats.every(cat => {
        let tile = this.world.getTile(3, cat.target.x, cat.target.y);
        return tile.name == "exit";
      });
      if (
        win === true &&
        this.cats.length >= this.world.findTile(3, 8).length &&
        !this.won
      ) {
        this.won = true;

        if (
          this.world.maps["map_" + (this.world.current_level + 1)] !== undefined
        ) {
          this.transition.start(
            0,
            Math.max(this.world.W / 2, this.world.H / 2),
            () => {
              this.world.current_level += 1;
              this.world.startScene("inGame");
            }
          );
        } else {
          this.transition.start(
            0,
            Math.max(this.world.W / 2, this.world.H / 2),
            () => {
              this.world.startScene("menu");
            }
          );
        }
      }
    };
    // destroy itself when animation is finish
    class Effect extends Entity {
      constructor(scene, sprite_data, x, y, callback) {
        super(scene, x * scene.world.tile_size, y * scene.world.tile_size);
        this.setSprite(sprite_data);
        this.sprite.addAnimation("full", sprite_data.frames);
        this.sprite.speed = 0.4;
        this.sprite.offset.y = -3;
        this.trigger = sprite_data.frames.length;
        this.callback = callback || undefined;
      }
      render() {
        if (this.sprite.current_frame + 1 === this.trigger) {
          if (this.callback !== undefined) {
            this.callback();
            this.callback = undefined;
          }
        }
        if (
          this.sprite.current_frame + 1 ===
          this.sprite.animations[this.sprite.current_animation].length
        ) {
          this.scene.effects.splice(this.scene.effects.indexOf(this), 1);
        }
        this.sprite.animate("full");
        this.display();
      }
    }
    class Cat extends Entity {
      constructor(scene, x, y) {
        super(scene, x * scene.world.tile_size, y * scene.world.tile_size);
        this.old_position = new Vector(x, y);
        this.target = new Vector(x, y);
        this.canBeControlled = true;
        this.inTranslation = false;
        this.lastDirection = new Vector(0, 0);
        this.isDead = false;
        // Trasnlation of the cat when they move
        this.transition = {
          start: new Date(),
          duration: 300,
          type: Util.easeInOutQuad,
          start_pos: new Vector()
        };
      }
      // apply translation on cat when necessary
      translation() {
        if (this.inTranslation) {
          // get current time !
          let time = new Date() - this.transition.start;
          if (time < this.transition.duration) {
            let x = this.transition.type(
              time,
              this.transition.start_pos.x,
              this.transition.target.x - this.transition.start_pos.x,
              this.transition.duration
            ),
              y = this.transition.type(
                time,
                this.transition.start_pos.y,
                this.transition.target.y - this.transition.start_pos.y,
                this.transition.duration
              );
            this.body.position = new Vector(x, y);
          } else {
            // apply position when translation is finish :) !
            this.old_position = this.target.copy();
            let next_move = this.target.copy();
            next_move.mult(this.world.tile_size);
            this.body.position = next_move;
            this.inTranslation = false;
            if (this.isDead) {
              // delete cat
              let spawn_data = {
                image: "water_splash",
                frames: [0, 1, 2, 3, 4, 5, 6, 7, 8],
                size: {
                  x: 20,
                  y: 32
                }
              };
              let spawn_effect = new Effect(
                this.scene,
                spawn_data,
                this.target.x,
                this.target.y - 1,
                () => {
                  this.scene.cats.splice(this.scene.cats.indexOf(this), 1);
                  this.scene.checkWin();
                }
              );
              spawn_effect.sprite.offset.y = 0;
              spawn_effect.trigger = 2;
              this.scene.effects.push(spawn_effect);
            }
            if (this.canBeControlled === false) {
              this.move(this.lastDirection.x, this.lastDirection.y);
              this.scene.collisionCats();
              this.applyMove();
            } else {
              // check arrows
              let current_tile = this.world.getTile(
                3,
                this.target.x,
                this.target.y
              );
              switch (current_tile.name) {
                case "arrowRight":
                  this.move(1, 0);
                  this.scene.collisionCats();
                  this.applyMove();
                  break;
                case "arrowLeft":
                  this.move(-1, 0);
                  this.scene.collisionCats();
                  this.applyMove();
                  break;
                case "arrowUp":
                  this.move(0, -1);
                  this.scene.collisionCats();
                  this.applyMove();
                  break;
                case "arrowDown":
                  this.move(0, 1);
                  this.scene.collisionCats();
                  this.applyMove();
                  break;
                default:
              }
            }
            // check if we won when a cat finish a step
            this.scene.checkWin();
          }
        }
      }
      move(x, y) {
        this.target = this.old_position.copy();
        let direction = new Vector(x, y);
        // get future position
        let future_position = this.target.copy();
        future_position.add(direction);
        let layers = this.world.terrain.layers;
        let future_tile = layers.map(layer => {
          let index = layers.indexOf(layer);
          return this.world.getTile(index, future_position.x, future_position.y);
        });
        let collision = future_tile.every(tile => {
          if (tile == false) {
            return tile == false;
          } else {
            return tile.collision === false;
          }
        });
        if (collision == true) {
          this.target.add(direction);
        }
        if (future_tile[3].name === "ice") {
          this.canBeControlled = false;
          this.transition.type = Util.linearTween;
          this.transition.duration = 100;
          return false;
        }
        if (future_tile[3].name === "trap") {
          let dust_data = {
            image: "dust_effect",
            frames: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
            size: {
              x: 32,
              y: 32
            }
          };
          let dust_effect = new Effect(
            this.scene,
            dust_data,
            this.target.x,
            this.target.y
          );
          this.scene.effects.push(dust_effect);

          this.world.terrain.layers[3].geometry[future_position.y][
            future_position.x
          ] = 10;
          // cache the map
          this.world.terrainCache(this.world.terrain.layers[3]);
          return false;
        }
        if (future_tile[1].name !== "ground") {
          this.transition.type = Util.easeInOutQuad;
          this.transition.duration = 200;
          this.isDead = true;
          return false;
        } else {
          this.canBeControlled = true;
          this.transition.type = Util.easeInOutQuad;
          this.transition.duration = 200;
          return false;
        }
      }
      applyMove() {
        // prevent cat to move if his target equal his actual position :V
        if (
          this.old_position.x === this.target.x &&
          this.old_position.y === this.target.y
        ) {
          this.canBeControlled = true;
          return false;
        }
        this.lastDirection = new Vector(
          this.target.x - this.old_position.x,
          this.target.y - this.old_position.y
        );
        this.shouldMove = false;
        this.transition.start_pos = this.old_position.copy();
        this.transition.start_pos.mult(this.world.tile_size);
        this.transition.target = this.target.copy();
        this.transition.target.mult(this.world.tile_size);
        this.transition.start = new Date();
        this.inTranslation = true;
      }
      checkOthers() {
        let others = this.scene.cats;
        let result = false;
        for (let i = 0; i < others.length; i++) {
          if (this === others[i]) continue;
          if (
            others[i].target.x === this.target.x &&
            others[i].target.y === this.target.y
          ) {
            result = true;
            break;
          }
        }
        return result;
      }
    }
    let controls = new Scene("controls");
    controls.keyEvents = function (event) {
      if (this.world.keys.KeyE) {
        this.world.startScene("menu");
      }
    };
    controls.init = function () {
      this.loop = false;
      this.controls = this.world.assets.image.controls.image;
    };
    controls.render = function () {
      this.world.clear("black");
      this.ctx.drawImage(this.controls, 0, 0);
      // notice
      this.world.setFont("origami_light");
      this.world.write(
        "[E] to exit",
        this.world.W / 2,
        this.world.H - 46,
        "center"
      );
    };

    let game = new Diorama(parameters);
    // global variables
    game.current_level = 1;
    // Add the different scenes here
    // the addScene function link the scene with the world (game)
    game.addScene(menu);
    game.addScene(levels);
    game.addScene(controls);
    game.addScene(inGame);
    game.ready();
    // everything start being loaded now !
    // the ready function must be called last !
    // Making the game full screen
    game.fullScreen();

    // 移动端触摸控制
    (function () {
      const controlButtons = document.querySelectorAll('.control-btn, .dpad-btn');
      const activeKeys = new Set();

      function triggerKey(keyCode, isDown) {
        if (isDown) {
          if (!activeKeys.has(keyCode)) {
            activeKeys.add(keyCode);
            game.keys[keyCode] = true;
            // 触发 keydown 事件
            const event = new KeyboardEvent('keydown', {
              code: keyCode,
              key: keyCode,
              bubbles: true,
              cancelable: true
            });
            game.keyDown(event);
          }
        } else {
          if (activeKeys.has(keyCode)) {
            activeKeys.delete(keyCode);
            game.keys[keyCode] = false;
            // 触发 keyup 事件
            const event = new KeyboardEvent('keyup', {
              code: keyCode,
              key: keyCode,
              bubbles: true,
              cancelable: true
            });
            game.keyUp(event);
          }
        }
      }

      controlButtons.forEach(btn => {
        const keyCode = btn.getAttribute('data-key');
        if (!keyCode) return;

        // 触摸开始
        btn.addEventListener('touchstart', (e) => {
          e.preventDefault();
          e.stopPropagation();
          triggerKey(keyCode, true);
        }, { passive: false });

        // 触摸结束
        btn.addEventListener('touchend', (e) => {
          e.preventDefault();
          e.stopPropagation();
          triggerKey(keyCode, false);
        }, { passive: false });

        // 触摸取消(手指移出按钮区域)
        btn.addEventListener('touchcancel', (e) => {
          e.preventDefault();
          e.stopPropagation();
          triggerKey(keyCode, false);
        }, { passive: false });

        // 鼠标事件(用于桌面端测试)
        btn.addEventListener('mousedown', (e) => {
          e.preventDefault();
          triggerKey(keyCode, true);
        });

        btn.addEventListener('mouseup', (e) => {
          e.preventDefault();
          triggerKey(keyCode, false);
        });

        btn.addEventListener('mouseleave', (e) => {
          e.preventDefault();
          triggerKey(keyCode, false);
        });
      });

      // 防止页面滚动和缩放(仅在控制按钮区域)
      document.addEventListener('touchmove', (e) => {
        if (e.target.closest('.mobile-controls')) {
          e.preventDefault();
        }
      }, { passive: false });

      // 防止双击缩放
      let lastTouchEnd = 0;
      document.addEventListener('touchend', (e) => {
        if (e.target.closest('.mobile-controls')) {
          const now = Date.now();
          if (now - lastTouchEnd <= 300) {
            e.preventDefault();
          }
          lastTouchEnd = now;
        }
      }, { passive: false });

      // 检测移动设备并显示控制按钮
      function checkMobileDevice() {
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
          (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
          window.innerWidth <= 768;

        const controls = document.querySelector('.mobile-controls');
        if (controls) {
          if (isMobile) {
            controls.style.display = 'flex';
          } else {
            controls.style.display = 'none';
          }
        }
      }

      // 初始检查和窗口大小改变时检查
      checkMobileDevice();
      window.addEventListener('resize', checkMobileDevice);
      window.addEventListener('orientationchange', () => {
        setTimeout(checkMobileDevice, 100);
      });
    })();
  </script>

</body>

</html>