/**
* @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));
}