neko/neko.js

283 lines
7.0 KiB
JavaScript

// Description: Neko
// Author: Jeff Clement
// License: MIT
// Size of the neko, in pixels
const nekoSize = 32;
// Speed of running animation
const runSpeed = 0.2;
// The neko statemachine
const stateMachine = {
sleep: {
images: ["sleep1", "sleep2"],
imageInterval: 1,
click: "awake",
},
yawn: {
images: ["yawn"],
nextState: ["sleep"],
nextStateDelay: 1,
},
awake: {
images: ["alert"],
nextState: ["normal"],
nextStateDelay: 1,
},
nscratch: {
images: ["nscratch1", "nscratch2"],
imageInterval: 0.25,
nextState: ["normal"],
nextStateDelay: 2,
click: "dying", // OMG. Don't click an itchin' neko!
},
escratch: {
images: ["escratch1", "escratch2"],
imageInterval: 0.25,
nextState: ["normal"],
nextStateDelay: 2,
click: "dying", // OMG. Don't click an itchin' neko!
},
sscratch: {
images: ["sscratch1", "sscratch2"],
imageInterval: 0.25,
nextState: ["normal"],
nextStateDelay: 2,
click: "dying", // OMG. Don't click an itchin' neko!
},
wscratch: {
images: ["wscratch1", "wscratch2"],
imageInterval: 0.25,
nextState: ["normal"],
nextStateDelay: 2,
click: "dying", // OMG. Don't click an itchin' neko!
},
itch: {
images: ["itch1", "itch2"],
imageInterval: 0.25,
nextState: ["normal"],
nextStateDelay: 2,
click: "dying", // OMG. Don't click an itchin' neko!
},
normal: {
awake: true,
images: ["still"],
nextState: [
"normal", "normal", "normal", "normal", "normal", "normal",
"itch", "nscratch", "escratch", "sscratch", "wscratch",
"yawn", "yawn",
],
nextStateDelay: 1,
},
nrun: {
awake: true,
imageInterval: runSpeed,
images: ["nrun1", "nrun2"],
},
nerun: {
awake: true,
imageInterval: runSpeed,
images: ["nerun1", "nerun2"],
},
erun: {
awake: true,
imageInterval: runSpeed,
images: ["erun1", "erun2"],
},
serun: {
awake: true,
imageInterval: runSpeed,
images: ["serun1", "serun2"],
},
srun: {
awake: true,
imageInterval: runSpeed,
images: ["srun1", "srun2"],
},
swrun: {
awake: true,
imageInterval: runSpeed,
images: ["swrun1", "swrun2"],
},
wrun: {
awake: true,
imageInterval: runSpeed,
images: ["wrun1", "wrun2"],
},
nwrun: {
awake: true,
imageInterval: runSpeed,
images: ["nwrun1", "nwrun2"],
},
dying: {
images: ["alert", "dying1", "dying2", "dying3", "dying4", "dying5"],
imageInterval: 0.5,
nextState: ["dead"],
nextStateDelay: 3,
},
dead: {
images: ["dying6"],
imageInterval: 0.5,
},
};
// Preload all the images
const scriptRoot = document.currentScript.src.substring(
0,
document.currentScript.src.lastIndexOf("/") + 1
);
var images = {};
for (let state in stateMachine) {
stateMachine[state].images.forEach(function (x) {
let img = new Image(nekoSize, nekoSize);
img.src = scriptRoot + "/res/" + x + ".gif";
images[x] = img;
});
}
class Neko {
// Neko's current state
state = null;
// Neko's current animation frame and timer to flip to the next one
animationInterval = null;
animationIndex = 0;
// Timer to switch to the next state
nextStateDelay = null;
// where is Neko heading (current mouse position, or last press)
targetX = -1;
targetY = -1;
constructor(
x = window.innerWidth - nekoSize,
y = window.innerHeight - nekoSize
) {
this.x = x;
this.y = y;
// build up the placeholder for the neko in the DOM
this.image = new Image(nekoSize, nekoSize);
this.image.style.position = "fixed";
this.image.onclick = this.handleClick.bind(this);
document.body.appendChild(this.image);
// start the movement loop
setInterval(this.update.bind(this), 100);
// hook into event handlers
window.addEventListener("resize", this.handleResize.bind(this));
window.addEventListener("mousemove", this.handleMouseMove.bind(this));
window.addEventListener("touchstart", this.handleTouch.bind(this));
// start the neko in the sleep state
this.setState("sleep");
// force a resize
this.handleResize();
}
setState(state) {
clearInterval(this.animationInterval);
clearTimeout(this.nextStateDelay);
this.state = state;
this.animationIndex = 0;
if (stateMachine[state].images.length > 1) {
this.animationInterval = setInterval(
this.nextFrame.bind(this),
(stateMachine[state].imageInterval || 1) * 1000
);
}
if (
stateMachine[state].nextState &&
stateMachine[state].nextState.length > 0
) {
this.nextStateDelay = setTimeout(() => {
this.setState(
stateMachine[state].nextState[
Math.floor(Math.random() * stateMachine[state].nextState.length)
]
);
}, (stateMachine[state].nextStateDelay || 1) * 1000);
}
}
nextFrame() {
this.animationIndex =
(this.animationIndex + 1) % stateMachine[this.state].images.length;
}
handleTouch(event) {
this.targetX = event.touches[0].clientX - nekoSize / 2;
this.targetY = event.touches[0].clientY - nekoSize / 2;
}
handleMouseMove(event) {
this.targetX = event.clientX - nekoSize / 2;
this.targetY = event.clientY - nekoSize / 2;
}
handleClick() {
if (stateMachine[this.state].click) {
this.setState(stateMachine[this.state].click);
}
}
handleResize() {
// adjust Neko's X and Y speed based on window size
this.speedX = ((document.body.clientWidth / 100) * nekoSize) / 32.0;
this.speedY = ((document.body.clientHeight / 100) * nekoSize) / 32.0;
// keep Neko on the screen
if (this.x > document.body.clientWidth - nekoSize) {
this.x = document.body.clientWidth - nekoSize;
}
if (this.y > document.body.clientHeight - nekoSize) {
this.y = document.body.clientHeight - nekoSize;
}
}
update() {
const state = stateMachine[this.state];
if (state.awake) {
const distanceX = this.targetX - this.x;
const distanceY = this.targetY - this.y;
// If we're close enough to the target, stop moving
const dx = Math.abs(distanceX) < this.speedX ? 0 : Math.sign(distanceX);
const dy = Math.abs(distanceY) < this.speedY ? 0 : Math.sign(distanceY);
// determine our new state
var newState = "normal";
if (dx == 1 && dy == 0) newState = "erun";
if (dx == 1 && dy == 1) newState = "serun";
if (dx == 0 && dy == 1) newState = "srun";
if (dx == -1 && dy == 1) newState = "swrun";
if (dx == -1 && dy == 0) newState = "wrun";
if (dx == -1 && dy == -1) newState = "nwrun";
if (dx == 0 && dy == -1) newState = "nrun";
if (dx == 1 && dy == -1) newState = "nerun";
if (newState != this.state) {
this.setState(newState);
}
// move Neko, if required
this.x += dx * this.speedX;
this.y += dy * this.speedY;
}
// Draw the neko
this.image.src = images[state.images[this.animationIndex]].src;
this.image.style.top = this.y + "px";
this.image.style.left = this.x + "px";
}
}