Source: sprites-1.js

/**
 * @fileoverview Uses string in base[3] to select a sprite.
 * This module can't update the board module directory, so may as well make the sprites object local to it.
 * @author Robert Laing
 * @module sprites
 */

import * as squares from "/js/modules/squares-1.js";

const monsters = {
  "bee": {
    "width": 64,
    "height": 64,
    "image": {
      "move": new Image(),
      "attack": new Image(),
      "death": new Image()
    },
    "src": {
      "move": "/sprite-sheets/bee.png",
      "attack": "/sprite-sheets/bee.png",
      "death": "/sprite-sheets/bee.png"
    },
    "loaded": {
      "move": false,
      "attack": false,
      "death": false
    },
    "frames": {
      "move": [[0,0],[64,0],[128,0],[192,0],[256,0],[320,0],[384,0],[448,0]],
      "attack": [[0,64],[64,64],[128,64],[192,64],[256,64],[320,64],[384,64]],
      "death": [[0,128],[64,128],[128,128],[192,128],[256,128],[320,128],[384,128],[448,128]]
    }
  },
  "hornet": {
    "width": 64,
    "height": 64,
    "image": {
      "move": new Image(),
      "attack": new Image(),
      "death": new Image()
    },
    "src": {
      "move": "/sprite-sheets/hornet-move.png",
      "attack": "/sprite-sheets/hornet-attack.png",
      "death": "/sprite-sheets/hornet-death.png"
    },
    "loaded": {
      "move": false,
      "attack": false,
      "death": false
    },
    "frames": {
      "move": [[0,0],[64,0],[128,0],[192,0],[256,0],[320,0],[384,0],[448,0]],
      "attack": [[0,0],[64,0],[128,0],[192,0],[256,0],[320,0],[384,0],[448,0]],
      "death": [
        [0,0],[64,0],[128,0],[192,0],
        [0,64],[64,64],[128,64],[192,64],
        [0,128],[64,128],[128,128],[192,128],
        [0,192],[64,192],[128,192],[192,192]
      ]
    }
  }
};

// the main portion sees these as sprites.secondHand etc, so no need to make them properties of an object
let busy = false;
let secondHand = 0;
let minuteHand = 0;
let spriteArray = [];
let mq = [];
let nexts = [];
let move_count = 0;
let game;
let rows;
let columns;
let cellLength;
let spriteTypes;

function load(sprite) {
  monsters[sprite].image.move.src = monsters[sprite].src.move;
  monsters[sprite].image.attack.src = monsters[sprite].src.attack;
  monsters[sprite].image.death.src = monsters[sprite].src.death;
  monsters[sprite].image.move.addEventListener("load", () => {monsters[sprite].loaded.move = true});
  monsters[sprite].image.attack.addEventListener("load", () => {monsters[sprite].loaded.attack = true});
  monsters[sprite].image.attack.addEventListener("load", () => {monsters[sprite].loaded.death = true});
}


export function send(message) {
  mq.push(message);
}

function enqueue(message) {
  const main_mq = JSON.parse(localStorage[`${game}_mq`]);
  main_mq.push(message);
  localStorage[`${game}_mq`] = JSON.stringify(main_mq);
}

export function init(board) {
  game = board.game;
  spriteTypes = structuredClone(board.sprite_type);
  for (const value of Object.values(spriteTypes)) {
    load(value);
  }
  secondHand = 0;
  minuteHand = 0;
  mq = [];
  nexts = [];
  rows = structuredClone(board.rows);
  columns = structuredClone(board.columns);
  cellLength = board.squareSideLength;
  busy = false;
  sync(board.state);
}

function endMove(sprite) {
  move_count += 1;
  const [row_idx, col_idx] = squares.xy2rowcol(sprite.x2, sprite.y2, cellLength);
  sprite.base[1] = columns[col_idx];
  sprite.base[2] = rows[row_idx];
  sprite.deltaX = 0.0;
  sprite.deltaY = 0.0;
  sprite.x1 = sprite.x2;
  sprite.y1 = sprite.y2;
  sprite.velocity = 0.0;
  sprite.status = "unselected";
  delete sprite.move;
  if (busy) {
    busy = false;
    enqueue(["animationEnd"]);
  }
}

export function update() {
  // slow default requestAnimationFrame loop speed of 60 ticks a second to about 16/frames a second
  secondHand += 1;            // this clocks faster than the "minuteHand" to slow animation
  if (secondHand === 5) {  
    secondHand = 0;
    minuteHand += 1;
    if (minuteHand === 15) {  // assumes 16 frame limit per sprite, may need to be made bigger.
      minuteHand = 0;
    }
  }
  // sync with next
  if (nexts.length > 0 && !busy) {
    if (move_count === nexts[0].move_count) {
      const next = nexts.shift();
      sync(next.state);
    }
  }
  // draw new action if free
  if (mq.length > 0 && !busy) {
    const action = mq.shift();
    switch (action[0]) {
      case "next":
        if (move_count === action[1].move_count) {
          sync(action[1].state);
        } else {
          nexts.push(action[1]);
        }
        break;
      case "move":
        move(action[1]);
        break;
      case "selectable":
        selectables();
        break;
      case "selected":
        selected(action[1]);
        break;
      default:
        window.container.textContent = `Unknown sprite action ${action[1]}`
    }
  }
  // update each sprite in array
  spriteArray.forEach(function(sprite) {
    // movement
    if (sprite.action === "move" && sprite.velocity > 0.0) {
      if (Math.hypot(sprite.x2 - sprite.x1, sprite.y2 - sprite.y1) <= sprite.velocity) {
        endMove(sprite);
      } else {
        sprite.x1 += sprite.deltaX;
        sprite.y1 += sprite.deltaY;
      }
    }
    // rotation
    if (Math.abs(sprite.deltaAngle) > 0.0) {
      if (Math.abs(sprite.angle1 - sprite.angle2) <= 2.0) {
        sprite.deltaAngle = 0.0;
        sprite.angle1 = sprite.angle2;
      }
        sprite.angle1 += sprite.deltaAngle;
        if (sprite.angle1 > 360.0) {
          sprite.angle1 -= 360.0;
        }
        if (sprite.angle1 < 0.0) {
          sprite.angle1 += 360.0;
        }
        if (Math.abs(sprite.angle1 - 360.0) < 1.0) {
          sprite.angle1 = 0.0;
        }
    }
    // collision detection
    spriteArray.forEach(function(sprite2) {
      if ( JSON.stringify(sprite.base) !== JSON.stringify(sprite2.base) &&
           Math.hypot(sprite.x1 - sprite2.x1, sprite.y1 - sprite2.y1) <= sprite.width ) {
        collision(sprite, sprite2);
      }
    });
    // advance frame/status
    if (sprite.tick !== minuteHand) {
      if (sprite.status !== "unselected") {
        sprite.frame += 1;
        if (sprite.frame === monsters[sprite.type].frames[sprite.action].length) {
          sprite.frame = 0;
        }
      }
      sprite.tick = minuteHand;
    };
  });
  // remove dead sprites
  spriteArray = spriteArray.filter((sprite) => sprite.live);
}

function collision(sprite1, sprite2) {
  if ( sprite1.status === "moving" && sprite2.status === "defending") {
    sprite1.action = "attack";
    sprite1.frame = 0;
    sprite2.status = "eliminated";
    sprite2.action = "death";
    sprite2.frame = 0;
  }
  if ( sprite2.status === "eliminated" && sprite2.frame === monsters[sprite2.type].frames.death.length -1) {
    sprite1.action = "move";
    sprite1.frame = 0;
    sprite2.live = false;
  }
}

export function resize(scale) {
  spriteArray.forEach(function(sprite) {
    sprite.x1 *= scale;
    sprite.y1 *= scale;
    sprite.x2 *= scale;
    sprite.y2 *= scale;
    sprite.width *= scale;
    sprite.height *= scale;
    sprite.deltaX *= scale;
    sprite.deltaY *= scale;
    sprite.velocity *= scale;
  });
}

function angleDegrees(x1, y1, x2, y2) {
  let angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
  if (angle < 0.0) {
    return 360 + angle;
  }
  return angle;
}

function turnDirection(angle1, angle2) {
  if (Math.abs(angle2) < 1.0 || Math.abs(360 - angle2) < 1.0) { // approximately 0, ie right
    if (angle1 < 180.00) {
      return -1.0
    } else {
      return 1.0
    }
  }
  if (angle2 > angle1) {
    return  1.0;
  } else {
    return -1.0;
  }
}


// note base[3] must be a string, so for focus the array of pieces needs to be stringified and parsed
function sync(state) {
  let sprite;
  let centerX;
  let centerY;
  let angle;
  spriteArray = [];
  state.forEach(function(base) {
    if (base[0] === "cell") {
      [centerX, centerY] = squares.colrow2xy(columns.indexOf(base[1]), rows.indexOf(base[2]), cellLength);
      sprite = {
        "type": spriteTypes[base[3]],
        "x1": centerX,
        "y1": centerY,
        "x2": centerX,
        "y2": centerY,
        "deltaX": 0.0,
        "deltaY": 0.0,
        "velocity": 0.0,
        "angle1": angleDegrees(centerX, centerY, window.canvas.width/2, window.canvas.width/2),
        "angle2": angleDegrees(centerX, centerY, window.canvas.width/2, window.canvas.width/2),
        "deltaAngle": 0.0,
        "action": "move",
        "width": cellLength * 0.8,
        "height": cellLength * 0.8,
        "tick": minuteHand,
        "frame": 0,
        "base": base,
        "status": "unselected",
        "live": true
      };
      spriteArray.push(sprite);
    }
  });
}

// eg {"black": ["move","b",1,"b",3]}
function move(move) {
  const player = Object.keys(move)[0];
  const [action, fromCol, fromRow, toCol, toRow] = Object.values(move)[0];
  const [x1, y1] = squares.colrow2xy(columns.indexOf(fromCol), rows.indexOf(fromRow), cellLength);
  const [x2, y2] = squares.colrow2xy(columns.indexOf(toCol), rows.indexOf(toRow), cellLength);
  const radians = Math.atan2(y2 - y1, x2 - x1);
  const attacker = spriteArray.filter((sprite) => JSON.stringify(["cell", fromCol, fromRow, player]) === JSON.stringify(sprite.base))[0];
  let defender = spriteArray.filter((sprite) => sprite.base[1] === toCol && sprite.base[2] === toRow);
  if (defender.length === 1) {
    defender[0].status = "defending";
    defender[0].action = "attack";
    defender[0].angle2 = angleDegrees(x2, y2, x1, y1);
    defender[0].deltaAngle = turnDirection(defender[0].angle1, defender[0].angle2);
  }
  attacker.x2 = x2;
  attacker.y2 = y2;
  attacker.velocity = cellLength/150;
  attacker.deltaX = Math.cos(radians) * attacker.velocity;
  attacker.deltaY = Math.sin(radians) * attacker.velocity;
  attacker.angle2 = angleDegrees(x1, y1, x2, y2);
  attacker.deltaAngle = turnDirection(attacker.angle1, attacker.angle2);
  attacker.status = "moving";
  attacker.action = "move";
  attacker.move = move;
  busy = true;
  enqueue(["animationStart"]);
}

function selectables() {
  const legals = JSON.parse(localStorage[`${game}_legals`])
  let player, action, fromCol, fromRow, toCol, toRow, selectable;
  legals.forEach(function(legal) {
    player = Object.keys(legal)[0];
    if (legal[player] !== "noop") {
      [action, fromCol, fromRow, toCol, toRow] = legal[player];
      selectable = spriteArray.filter((sprite) => JSON.stringify(["cell", fromCol, fromRow, player]) === JSON.stringify(sprite.base))[0];
      selectable.status = "selectable";
      selectable.action = "move";
    }
  });
}

function selected(legal) {
  const player = Object.keys(legal)[0];
  const [action, fromCol, fromRow, toCol, toRow] = Object.values(legal)[0];
  spriteArray.forEach(function(sprite) {
    if (JSON.stringify(["cell", fromCol, fromRow, player]) === JSON.stringify(sprite.base)) {
      sprite.status = "selected";
      sprite.action = "attack";
    } else {
      sprite.status = "unselected";
    }
  });
}


function drawSprite(sprite) {
  if (monsters[sprite.type].loaded[sprite.action]) {
    window.ctx.save();
    window.ctx.translate(sprite.x1, sprite.y1);
    window.ctx.rotate(Math.PI/2 + (sprite.angle1 * Math.PI/180));
    window.ctx.drawImage(
      monsters[sprite.type].image[sprite.action], 
      monsters[sprite.type].frames[sprite.action][sprite.frame][0],
      monsters[sprite.type].frames[sprite.action][sprite.frame][1],
      monsters[sprite.type].width, 
      monsters[sprite.type].height, 
      -sprite.width/2, 
      -sprite.height/2, 
      sprite.width, 
      sprite.height
    );
    window.ctx.restore();
  }
}

export function drawSprites() {
  spriteArray.forEach((sprite) => drawSprite(sprite));
}