|
@ -1,3 +1,10 @@ |
|
|
|
|
|
const Orientation = { |
|
|
|
|
|
UP: 'up', |
|
|
|
|
|
DOWN: 'down', |
|
|
|
|
|
LEFT: 'left', |
|
|
|
|
|
RIGHT: 'right' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
class Input { |
|
|
class Input { |
|
|
constructor() { |
|
|
constructor() { |
|
|
this.left = false; |
|
|
this.left = false; |
|
@ -13,6 +20,8 @@ class Input { |
|
|
this.select = false; |
|
|
this.select = false; |
|
|
this.start = false; |
|
|
this.start = false; |
|
|
|
|
|
|
|
|
|
|
|
this.leftArrowPressed = false; |
|
|
|
|
|
this.rightArrowPressed = false; |
|
|
window.addEventListener('gamepadconnected', this.gamepadConnected); |
|
|
window.addEventListener('gamepadconnected', this.gamepadConnected); |
|
|
window.addEventListener('gamepaddisconnected', this.gamepadDisconnected); |
|
|
window.addEventListener('gamepaddisconnected', this.gamepadDisconnected); |
|
|
} |
|
|
} |
|
@ -140,6 +149,16 @@ class Graphics { |
|
|
this.ctx_.font = '' + size + 'px monospace'; |
|
|
this.ctx_.font = '' + size + 'px monospace'; |
|
|
this.ctx_.fillText(string, x, y); |
|
|
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 { |
|
|
class FpsCounter { |
|
@ -162,7 +181,8 @@ class FpsCounter { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
draw(gfx) { |
|
|
draw(gfx) { |
|
|
gfx.text('FPS: ' + Math.round(this.fps), 8, 16, 16, 'yellow'); |
|
|
|
|
|
|
|
|
const fpsDiv = document.getElementById('fps'); |
|
|
|
|
|
fpsDiv.innerText = 'FPS: ' + Math.round(this.fps); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -171,27 +191,214 @@ class World { |
|
|
this.state_ = null; |
|
|
this.state_ = null; |
|
|
this.fpsCounter_ = new FpsCounter(); |
|
|
this.fpsCounter_ = new FpsCounter(); |
|
|
this.input_ = new Input(); |
|
|
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(); |
|
|
this.gamepadRenderer_ = new GamepadRenderer(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
update(timestampMs) { |
|
|
update(timestampMs) { |
|
|
this.fpsCounter_.update(timestampMs); |
|
|
this.fpsCounter_.update(timestampMs); |
|
|
this.input_.update(); |
|
|
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) { |
|
|
draw(gfx) { |
|
|
this.gamepadRenderer_.draw(gfx, this.input_); |
|
|
|
|
|
|
|
|
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); |
|
|
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 ", |
|
|
|
|
|
" "]; |
|
|
|
|
|
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 { |
|
|
class GamepadRenderer { |
|
|
draw(gfx, input) { |
|
|
draw(gfx, input) { |
|
|
const centerX = gfx.width / 2; |
|
|
const centerX = gfx.width / 2; |
|
|
const centerY = gfx.height / 2; |
|
|
const centerY = gfx.height / 2; |
|
|
|
|
|
|
|
|
gfx.fill('black'); |
|
|
|
|
|
|
|
|
|
|
|
// Select & Start
|
|
|
// Select & Start
|
|
|
gfx.circle(centerX + 12, centerY, 8, input.start ? 'cyan' : 'grey'); |
|
|
gfx.circle(centerX + 12, centerY, 8, input.start ? 'cyan' : 'grey'); |
|
|
gfx.circle(centerX - 12, centerY, 8, input.select ? 'cyan' : 'grey'); |
|
|
gfx.circle(centerX - 12, centerY, 8, input.select ? 'cyan' : 'grey'); |
|
|