f8a3be556d
fix a map typo.
501 lines
13 KiB
JavaScript
501 lines
13 KiB
JavaScript
const Orientation = {
|
|
UP: 'up',
|
|
DOWN: 'down',
|
|
LEFT: 'left',
|
|
RIGHT: 'right'
|
|
}
|
|
|
|
// TODO: make these not global.
|
|
let upArrowPressed = false;
|
|
let downArrowPressed = false;
|
|
let leftArrowPressed = false;
|
|
let rightArrowPressed = false;
|
|
|
|
class Input {
|
|
constructor() {
|
|
this.up = false;
|
|
this.down = false;
|
|
this.left = false;
|
|
this.right = false;
|
|
this.a = false;
|
|
this.b = false;
|
|
this.x = false;
|
|
this.y = false;
|
|
this.l = false;
|
|
this.r = false;
|
|
this.select = false;
|
|
this.start = false;
|
|
|
|
window.addEventListener('gamepadconnected', this.gamepadConnected);
|
|
window.addEventListener('gamepaddisconnected', this.gamepadDisconnected);
|
|
document.addEventListener('keydown', this.keyDown);
|
|
document.addEventListener('keyup', this.keyUp);
|
|
}
|
|
|
|
keyDown(e) {
|
|
if (e.key == 'ArrowUp' || e.key == 'w') {
|
|
upArrowPressed = true;
|
|
}
|
|
if (e.key == 'ArrowDown' || e.key == 's') {
|
|
downArrowPressed = true;
|
|
}
|
|
if (e.key == 'ArrowLeft' || e.key == 'a') {
|
|
leftArrowPressed = true;
|
|
}
|
|
if (e.key == 'ArrowRight' || e.key == 'd') {
|
|
rightArrowPressed = true;
|
|
}
|
|
}
|
|
|
|
keyUp(e) {
|
|
if (e.key == 'ArrowUp' || e.key == 'w') {
|
|
upArrowPressed = false;
|
|
}
|
|
if (e.key == 'ArrowDown' || e.key == 's') {
|
|
downArrowPressed = false;
|
|
}
|
|
if (e.key == 'ArrowLeft' || e.key == 'a') {
|
|
leftArrowPressed = false;
|
|
}
|
|
if (e.key == 'ArrowRight' || e.key == 'd') {
|
|
rightArrowPressed = false;
|
|
}
|
|
}
|
|
|
|
update() {
|
|
// TODO: have a config screen instead of hard-coding the 8Bitdo SNES30 pad.
|
|
// TODO: handle connects / disconnects more correctly.
|
|
|
|
this.up = upArrowPressed;
|
|
this.down = downArrowPressed;
|
|
this.left = leftArrowPressed;
|
|
this.right = rightArrowPressed;
|
|
|
|
const gamepad = navigator.getGamepads()[0];
|
|
if (gamepad == null || !gamepad.connected || gamepad.axes.length < 2 ||
|
|
gamepad.buttons.length < 12) {
|
|
return;
|
|
}
|
|
this.up |= gamepad.axes[1] < 0;
|
|
this.down |= gamepad.axes[1] > 0;
|
|
this.left |= gamepad.axes[0] < 0;
|
|
this.right |= gamepad.axes[0] > 0;
|
|
this.a = gamepad.buttons[0].pressed;
|
|
this.b = gamepad.buttons[1].pressed;
|
|
this.x = gamepad.buttons[3].pressed;
|
|
this.y = gamepad.buttons[4].pressed;
|
|
this.l = gamepad.buttons[6].pressed;
|
|
this.r = gamepad.buttons[7].pressed;
|
|
this.select = gamepad.buttons[10].pressed;
|
|
this.start = gamepad.buttons[11].pressed;
|
|
debug(this.toString());
|
|
}
|
|
|
|
gamepadConnected(e) {
|
|
debug('gamepad connected! :)');
|
|
console.log('gamepad connected @ index %d: %d buttons, %d axes\n[%s]',
|
|
e.gamepad.index, e.gamepad.buttons.length, e.gamepad.axes.length,
|
|
e.gamepad.id);
|
|
}
|
|
|
|
gamepadDisconnected(e) {
|
|
debug('gamepad disconnected :(');
|
|
console.log('gamepad disconnected @ index %d:\n[%s]', e.gamepad.index,
|
|
e.gamepad.id);
|
|
}
|
|
|
|
toString() {
|
|
let result = '';
|
|
|
|
if (this.up) {
|
|
result += '^';
|
|
} else if (this.down) {
|
|
result += 'v';
|
|
} else {
|
|
result += '-';
|
|
}
|
|
|
|
if (this.left) {
|
|
result += '<';
|
|
} else if (this.right) {
|
|
result += '>';
|
|
} else {
|
|
result += '-';
|
|
}
|
|
|
|
result += ' ';
|
|
|
|
if (this.a) {
|
|
result += 'A';
|
|
}
|
|
if (this.b) {
|
|
result += 'B';
|
|
}
|
|
if (this.x) {
|
|
result += 'X';
|
|
}
|
|
if (this.y) {
|
|
result += 'Y';
|
|
}
|
|
if (this.l) {
|
|
result += 'L';
|
|
}
|
|
if (this.r) {
|
|
result += 'R';
|
|
}
|
|
if (this.select) {
|
|
result += 's';
|
|
}
|
|
if (this.start) {
|
|
result += 'S';
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class Graphics {
|
|
constructor(canvas) {
|
|
this.canvas_ = canvas;
|
|
this.ctx_ = canvas.getContext('2d');
|
|
}
|
|
|
|
get width() {
|
|
return this.canvas_.width;
|
|
}
|
|
|
|
get height() {
|
|
return this.canvas_.height;
|
|
}
|
|
|
|
fill(color) {
|
|
this.ctx_.fillStyle = color;
|
|
this.ctx_.beginPath();
|
|
this.ctx_.rect(0, 0, this.canvas_.width, this.canvas_.height);
|
|
this.ctx_.fill();
|
|
this.ctx_.closePath();
|
|
}
|
|
|
|
circle(x, y, radius, color) {
|
|
this.ctx_.fillStyle = color;
|
|
this.ctx_.beginPath();
|
|
this.ctx_.arc(x, y, radius, 0, 2 * Math.PI)
|
|
this.ctx_.fill();
|
|
this.ctx_.closePath();
|
|
}
|
|
|
|
// TODO: replace with custom sprite-based text rendering.
|
|
text(string, x, y, size, color) {
|
|
this.ctx_.imageSmoothingEnabled = false;
|
|
this.ctx_.fillStyle = color;
|
|
this.ctx_.font = '' + size + 'px monospace';
|
|
this.ctx_.fillText(string, x, y);
|
|
}
|
|
|
|
drawImage(image, dx, dy) {
|
|
const src = image[0];
|
|
const sx = image[1];
|
|
const sy = image[2];
|
|
const width = image[3];
|
|
const height = image[4];
|
|
this.ctx_.drawImage(
|
|
src, sx, sy, width, height, dx, dy, width, height);
|
|
}
|
|
}
|
|
|
|
class FpsCounter {
|
|
constructor() {
|
|
this.fps = 0;
|
|
this.frameTimes_ = new Array(60);
|
|
this.idx_ = 0;
|
|
}
|
|
|
|
update(timestampMs) {
|
|
if (this.frameTimes_[this.idx_]) {
|
|
const timeElapsed = (timestampMs - this.frameTimes_[this.idx_]) / 1000;
|
|
this.fps = this.frameTimes_.length / timeElapsed;
|
|
}
|
|
this.frameTimes_[this.idx_] = timestampMs;
|
|
this.idx_++;
|
|
if (this.idx_ == this.frameTimes_.length) {
|
|
this.idx_ = 0;
|
|
}
|
|
}
|
|
|
|
draw(gfx) {
|
|
const fpsDiv = document.getElementById('fps');
|
|
fpsDiv.innerText = 'FPS: ' + Math.round(this.fps);
|
|
}
|
|
}
|
|
|
|
class World {
|
|
constructor() {
|
|
this.state_ = null;
|
|
this.fpsCounter_ = new FpsCounter();
|
|
this.input_ = new Input();
|
|
this.player_ = new Player();
|
|
|
|
// TODO: move rendering stuff to a separate object.
|
|
this.resources_ = new Resources();
|
|
this.tileRenderer_ = new TileRenderer();
|
|
this.playerRenderer_ = new PlayerRenderer();
|
|
this.gamepadRenderer_ = new GamepadRenderer();
|
|
}
|
|
|
|
update(timestampMs) {
|
|
this.fpsCounter_.update(timestampMs);
|
|
this.input_.update();
|
|
if (this.input_.left) {
|
|
this.player_.moveLeft();
|
|
}
|
|
if (this.input_.right) {
|
|
this.player_.moveRight();
|
|
}
|
|
if (this.input_.up) {
|
|
this.player_.moveUp();
|
|
}
|
|
if (this.input_.down) {
|
|
this.player_.moveDown();
|
|
}
|
|
}
|
|
|
|
draw(gfx) {
|
|
gfx.fill('black');
|
|
this.tileRenderer_.draw(gfx, this.resources_.sprites);
|
|
this.playerRenderer_.draw(gfx, this.resources_.sprites, this.player_);
|
|
// this.gamepadRenderer_.draw(gfx, this.input_);
|
|
this.fpsCounter_.draw(gfx);
|
|
}
|
|
}
|
|
|
|
class Resources {
|
|
constructor() {
|
|
const atlantis = document.getElementById('atlantis');
|
|
const ghost = document.getElementById('ghost');
|
|
const ts = 16;
|
|
this.sprites = {
|
|
'ground0': [atlantis, 2 * ts, 1 * ts, 16, 16],
|
|
'ground1': [atlantis, 3 * ts, 1 * ts, 16, 16],
|
|
'ground2': [atlantis, 4 * ts, 1 * ts, 16, 16],
|
|
'ground3': [atlantis, 5 * ts, 1 * ts, 16, 16],
|
|
'ground4': [atlantis, 6 * ts, 1 * ts, 16, 16],
|
|
'ground5': [atlantis, 7 * ts, 1 * ts, 16, 16],
|
|
'ground6': [atlantis, 8 * ts, 1 * ts, 16, 16],
|
|
'rock0': [atlantis, 1 * ts, 2 * ts, 16, 16],
|
|
'rock1': [atlantis, 2 * ts, 2 * ts, 16, 16],
|
|
'rock2': [atlantis, 3 * ts, 2 * ts, 16, 16],
|
|
'anchor0': [atlantis, 21 * ts, 1 * ts, 16, 16],
|
|
'seaweed0': [atlantis, 20 * ts, 2 * ts, 16, 32],
|
|
'seaweed1': [atlantis, 16 * ts, 2 * ts, 16, 32],
|
|
'coral0': [atlantis, 15 * ts, 9 * ts, 32, 16],
|
|
'rockpile0': [atlantis, 17 * ts, 10 * ts, 32, 32],
|
|
|
|
'ghostdown0': [ghost, 0, 0, 24, 36],
|
|
'ghostdown1': [ghost, 26, 0, 24, 36],
|
|
'ghostdown2': [ghost, 52, 0, 24, 36],
|
|
'ghostleft0': [ghost, 0, 36, 24, 36],
|
|
'ghostleft1': [ghost, 26, 36, 24, 36],
|
|
'ghostleft2': [ghost, 52, 36, 24, 36],
|
|
'ghostright0': [ghost, 0, 72, 24, 36],
|
|
'ghostright1': [ghost, 26, 72, 24, 36],
|
|
'ghostright2': [ghost, 52, 72, 24, 36],
|
|
'ghostup0': [ghost, 0, 108, 24, 36],
|
|
'ghostup1': [ghost, 26, 108, 24, 36],
|
|
'ghostup2': [ghost, 52, 108, 24, 36],
|
|
}
|
|
}
|
|
}
|
|
|
|
class Player {
|
|
constructor() {
|
|
this.x = (256 - 26) / 2;
|
|
this.y = (224 - 36) / 2;
|
|
this.orientation = Orientation.DOWN;
|
|
}
|
|
|
|
moveLeft() {
|
|
this.orientation = Orientation.LEFT;
|
|
this.x -= 2;
|
|
if (this.x < -4) {
|
|
this.x = -4;
|
|
}
|
|
}
|
|
|
|
moveRight() {
|
|
this.orientation = Orientation.RIGHT;
|
|
this.x += 2;
|
|
if (this.x > 256 - 21) {
|
|
this.x = 256 - 21;
|
|
}
|
|
}
|
|
|
|
moveUp() {
|
|
this.orientation = Orientation.UP;
|
|
this.y -= 2;
|
|
if (this.y < -7) {
|
|
this.y = -7;
|
|
}
|
|
}
|
|
|
|
moveDown() {
|
|
this.orientation = Orientation.DOWN;
|
|
this.y += 2;
|
|
if (this.y > 224 - 36) {
|
|
this.y = 224 - 36;
|
|
}
|
|
}
|
|
}
|
|
|
|
class PlayerRenderer {
|
|
constructor() {
|
|
this.frameNum = 0;
|
|
}
|
|
|
|
draw(gfx, sprites, player) {
|
|
let spriteIndex = Math.floor((this.frameNum % 40) / 10);
|
|
if (spriteIndex == 3) { spriteIndex = 1; }
|
|
const spriteName = 'ghost' + player.orientation + spriteIndex;
|
|
gfx.drawImage(sprites[spriteName], player.x, player.y);
|
|
this.frameNum++;
|
|
}
|
|
}
|
|
|
|
class TileRenderer {
|
|
draw(gfx, sprites) {
|
|
const tileSize = 16;
|
|
const rows = gfx.height / tileSize;
|
|
const columns = gfx.width / tileSize;
|
|
const layer1 = ["-,*-...*'.,-_'`o",
|
|
"_..'-_**,',_.'oo",
|
|
"-*-''_-'o,0O_```",
|
|
"o`0_._,*O'`--'-'",
|
|
"`0O-_'',`o*o*`-,",
|
|
"*,`'---o'O'_*''-",
|
|
"'-.**.'_'`.,'-.'",
|
|
".O'``*``'`*,,_o`",
|
|
"_*_''*O'`_OO-_'o",
|
|
"0`0,*-,`_*'`O'*.",
|
|
".o'-*.*_',`,,`.'",
|
|
"`o`O',.`OO,*-'**",
|
|
"-..*'-''',*'.'.O",
|
|
"*-_'-0.--__O`O`_",
|
|
"*-_,O_'*'`*'_._.",
|
|
"-.*,`OO'_`'*-0-O"];
|
|
const layer2 = [" ",
|
|
" ",
|
|
" iil ",
|
|
" ",
|
|
" A ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" i ",
|
|
" l ",
|
|
" ",
|
|
" c R ",
|
|
" c "];
|
|
const spriteLookup = {
|
|
'.': sprites.ground0,
|
|
',': sprites.ground1,
|
|
'_': sprites.ground2,
|
|
'`': sprites.ground3,
|
|
'-': sprites.ground4,
|
|
'*': sprites.ground5,
|
|
"'": sprites.ground6,
|
|
'o': sprites.rock0,
|
|
'O': sprites.rock1,
|
|
'0': sprites.rock2,
|
|
'A': sprites.anchor0,
|
|
'i': sprites.seaweed0,
|
|
'l': sprites.seaweed1,
|
|
'c': sprites.coral0,
|
|
'R': sprites.rockpile0,
|
|
};
|
|
for (let j = 0; j < columns; j++) {
|
|
for (let i = 0; i < rows; i++) {
|
|
const dx = tileSize * j;
|
|
const dy = tileSize * i;
|
|
const sprite = spriteLookup[layer1[i][j]];
|
|
if (sprite) {
|
|
gfx.drawImage(sprite, dx, dy);
|
|
}
|
|
}
|
|
}
|
|
for (let j = 0; j < columns; j++) {
|
|
for (let i = 0; i < rows; i++) {
|
|
const dx = tileSize * j;
|
|
const dy = tileSize * i;
|
|
const sprite = spriteLookup[layer2[i][j]];
|
|
if (sprite) {
|
|
gfx.drawImage(sprite, dx, dy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class GamepadRenderer {
|
|
draw(gfx, input) {
|
|
const centerX = gfx.width / 2;
|
|
const centerY = gfx.height / 2;
|
|
|
|
// Select & Start
|
|
gfx.circle(centerX + 12, centerY, 8, input.start ? 'cyan' : 'grey');
|
|
gfx.circle(centerX - 12, centerY, 8, input.select ? 'cyan' : 'grey');
|
|
|
|
// Y X B A
|
|
gfx.circle(centerX + 48, centerY, 8, input.y ? 'cyan' : 'grey');
|
|
gfx.circle(centerX + 64, centerY - 16, 8, input.x ? 'cyan' : 'grey');
|
|
gfx.circle(centerX + 64, centerY + 16, 8, input.b ? 'cyan' : 'grey');
|
|
gfx.circle(centerX + 80, centerY, 8, input.a ? 'cyan' : 'grey');
|
|
|
|
// dpad
|
|
gfx.circle(centerX - 48, centerY, 8, input.right ? 'cyan' : 'grey');
|
|
gfx.circle(centerX - 64, centerY - 16, 8, input.up ? 'cyan' : 'grey');
|
|
gfx.circle(centerX - 64, centerY + 16, 8, input.down ? 'cyan' : 'grey');
|
|
gfx.circle(centerX - 80, centerY, 8, input.left ? 'cyan' : 'grey');
|
|
|
|
// L & R
|
|
gfx.circle(centerX + 30, centerY - 32, 8, input.r ? 'cyan' : 'grey');
|
|
gfx.circle(centerX - 30, centerY - 32, 8, input.l ? 'cyan' : 'grey');
|
|
}
|
|
}
|
|
|
|
function debug(message) {
|
|
const debugDiv = document.getElementById('debug');
|
|
debugDiv.innerText = message;
|
|
}
|
|
|
|
function loop(world, gfx) {
|
|
return timestampMs => {
|
|
world.update(timestampMs);
|
|
world.draw(gfx);
|
|
window.requestAnimationFrame(loop(world, gfx));
|
|
};
|
|
}
|
|
|
|
function setCanvasScale(scale) {
|
|
const snesWidth = 256;
|
|
const snesHeight = 224;
|
|
const canvas = document.getElementById('canvas');
|
|
canvas.style.width = '' + snesWidth * scale + 'px';
|
|
canvas.style.height = '' + snesHeight * scale + 'px';
|
|
}
|
|
|
|
function init() {
|
|
const world = new World();
|
|
const gfx = new Graphics(document.getElementById('canvas'));
|
|
|
|
document.getElementById('1x').onclick = () => setCanvasScale(1);
|
|
document.getElementById('2x').onclick = () => setCanvasScale(2);
|
|
document.getElementById('3x').onclick = () => setCanvasScale(3);
|
|
document.getElementById('4x').onclick = () => setCanvasScale(4);
|
|
document.getElementById('5x').onclick = () => setCanvasScale(5);
|
|
setCanvasScale(3);
|
|
window.requestAnimationFrame(loop(world, gfx));
|
|
debug('initialized!');
|
|
}
|
|
|
|
init();
|