Created
November 19, 2019 17:04
-
-
Save jasontwuk/ccf601581fe8235c7c033e3ff1af2311 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Ch16-exercise-GameOver</title> | |
<script src="levels.js"></script> | |
<!-- <link rel="stylesheet" href="css/game.css"> --> | |
<style> | |
.background { background: rgb(52, 166, 251); | |
table-layout: fixed; | |
border-spacing: 0; } | |
.background td { padding: 0; } | |
.lava { background: rgb(255, 100, 100); } | |
.wall { background: white; } | |
.actor { position: absolute; } | |
.coin { background: rgb(241, 229, 89); } | |
.player { background: rgb(64, 64, 64); } | |
.lost .player { | |
background: rgb(160, 64, 64); | |
} | |
.won .player { | |
box-shadow: -4px -7px 8px white, 4px -7px 8px white; | |
} | |
.game { | |
overflow: hidden; | |
max-width: 600px; | |
max-height: 450px; | |
position: relative; | |
} | |
</style> | |
</head> | |
<body> | |
<script> | |
let simpleLevelPlan = ` | |
...................... | |
..#................#.. | |
..#..............=.#.. | |
..#.........o.o....#.. | |
..#.@......#####...#.. | |
..#####............#.. | |
......#++++++++++++#.. | |
......##############.. | |
......................`; | |
class Level { | |
constructor(plan) { | |
let rows = plan.trim().split("\n").map(l => [...l]); | |
this.height = rows.length; | |
this.width = rows[0].length; | |
this.startActors = []; | |
this.rows = rows.map((row, y) => { | |
return row.map((ch, x) => { | |
let type = levelChars[ch]; | |
if (typeof type == "string") return type; | |
this.startActors.push( | |
type.create(new Vec(x, y), ch)); | |
return "empty"; | |
}); | |
}); | |
} | |
} | |
class State { | |
constructor(level, actors, status) { | |
this.level = level; | |
this.actors = actors; | |
this.status = status; | |
} | |
static start(level) { | |
return new State(level, level.startActors, "playing"); | |
} | |
get player() { | |
return this.actors.find(a => a.type == "player"); | |
} | |
} | |
class Vec { | |
constructor(x, y) { | |
this.x = x; this.y = y; | |
} | |
plus(other) { | |
return new Vec(this.x + other.x, this.y + other.y); | |
} | |
times(factor) { | |
return new Vec(this.x * factor, this.y * factor); | |
} | |
} | |
class Player { | |
constructor(pos, speed) { | |
this.pos = pos; | |
this.speed = speed; | |
} | |
get type() { return "player"; } | |
static create(pos) { | |
return new Player(pos.plus(new Vec(0, -0.5)), | |
new Vec(0, 0)); | |
} | |
} | |
Player.prototype.size = new Vec(0.8, 1.5); | |
class Lava { | |
constructor(pos, speed, reset) { | |
this.pos = pos; | |
this.speed = speed; | |
this.reset = reset; | |
} | |
get type() { return "lava"; } | |
static create(pos, ch) { | |
if (ch == "=") { | |
return new Lava(pos, new Vec(2, 0)); | |
} else if (ch == "|") { | |
return new Lava(pos, new Vec(0, 2)); | |
} else if (ch == "v") { | |
return new Lava(pos, new Vec(0, 3), pos); | |
} | |
} | |
} | |
Lava.prototype.size = new Vec(1, 1); | |
let Coin = class Coin { | |
constructor(pos, basePos, wobble) { | |
this.pos = pos; | |
this.basePos = basePos; | |
this.wobble = wobble; | |
} | |
get type() { return "coin"; } | |
static create(pos) { | |
let basePos = pos.plus(new Vec(0.2, 0.1)); | |
return new Coin(basePos, basePos, | |
Math.random() * Math.PI * 2); | |
} | |
} | |
Coin.prototype.size = new Vec(0.6, 0.6); | |
const levelChars = { | |
".": "empty", "#": "wall", "+": "lava", | |
"@": Player, "o": Coin, | |
"=": Lava, "|": Lava, "v": Lava | |
}; | |
function elt(name, attrs, ...children) { | |
let dom = document.createElement(name); | |
for (let attr of Object.keys(attrs)) { | |
dom.setAttribute(attr, attrs[attr]); | |
} | |
for (let child of children) { | |
dom.appendChild(child); | |
} | |
return dom; | |
} | |
class DOMDisplay { | |
constructor(parent, level) { | |
this.dom = elt("div", {class: "game"}, drawGrid(level)); | |
this.actorLayer = null; | |
parent.appendChild(this.dom); | |
} | |
clear() { this.dom.remove(); } | |
} | |
const scale = 20; | |
function drawGrid(level) { | |
return elt("table", { | |
class: "background", | |
style: `width: ${level.width * scale}px` | |
}, ...level.rows.map(row => | |
elt("tr", {style: `height: ${scale}px`}, | |
...row.map(type => elt("td", {class: type}))) | |
)); | |
} | |
function drawActors(actors) { | |
return elt("div", {}, ...actors.map(actor => { | |
let rect = elt("div", {class: `actor ${actor.type}`}); | |
rect.style.width = `${actor.size.x * scale}px`; | |
rect.style.height = `${actor.size.y * scale}px`; | |
rect.style.left = `${actor.pos.x * scale}px`; | |
rect.style.top = `${actor.pos.y * scale}px`; | |
return rect; | |
})); | |
} | |
DOMDisplay.prototype.syncState = function(state) { | |
if (this.actorLayer) this.actorLayer.remove(); | |
this.actorLayer = drawActors(state.actors); | |
this.dom.appendChild(this.actorLayer); | |
this.dom.className = `game ${state.status}`; | |
this.scrollPlayerIntoView(state); | |
}; | |
DOMDisplay.prototype.scrollPlayerIntoView = function(state) { | |
let width = this.dom.clientWidth; | |
let height = this.dom.clientHeight; | |
let margin = width / 3; | |
// The viewport | |
let left = this.dom.scrollLeft, right = left + width; | |
let top = this.dom.scrollTop, bottom = top + height; | |
let player = state.player; | |
let center = player.pos.plus(player.size.times(0.5)) | |
.times(scale); | |
if (center.x < left + margin) { | |
this.dom.scrollLeft = center.x - margin; | |
} else if (center.x > right - margin) { | |
this.dom.scrollLeft = center.x + margin - width; | |
} | |
if (center.y < top + margin) { | |
this.dom.scrollTop = center.y - margin; | |
} else if (center.y > bottom - margin) { | |
this.dom.scrollTop = center.y + margin - height; | |
} | |
}; | |
// let simpleLevel = new Level(simpleLevelPlan); | |
// // console.log(`${simpleLevel.width} by ${simpleLevel.height}`); | |
// // // → 22 by 9 | |
// console.log(simpleLevel); | |
// let display = new DOMDisplay(document.body, simpleLevel); | |
// console.log(display); | |
// display.syncState(State.start(simpleLevel)); | |
Level.prototype.touches = function(pos, size, type) { | |
var xStart = Math.floor(pos.x); | |
var xEnd = Math.ceil(pos.x + size.x); | |
var yStart = Math.floor(pos.y); | |
var yEnd = Math.ceil(pos.y + size.y); | |
for (var y = yStart; y < yEnd; y++) { | |
for (var x = xStart; x < xEnd; x++) { | |
let isOutside = x < 0 || x >= this.width || | |
y < 0 || y >= this.height; | |
let here = isOutside ? "wall" : this.rows[y][x]; | |
if (here == type) return true; | |
} | |
} | |
return false; | |
}; | |
State.prototype.update = function(time, keys) { | |
let actors = this.actors | |
.map(actor => actor.update(time, this, keys)); | |
let newState = new State(this.level, actors, this.status); | |
if (newState.status != "playing") return newState; | |
let player = newState.player; | |
if (this.level.touches(player.pos, player.size, "lava")) { | |
return new State(this.level, actors, "lost"); | |
} | |
for (let actor of actors) { | |
if (actor != player && overlap(actor, player)) { | |
newState = actor.collide(newState); | |
} | |
} | |
return newState; | |
}; | |
function overlap(actor1, actor2) { | |
return actor1.pos.x + actor1.size.x > actor2.pos.x && | |
actor1.pos.x < actor2.pos.x + actor2.size.x && | |
actor1.pos.y + actor1.size.y > actor2.pos.y && | |
actor1.pos.y < actor2.pos.y + actor2.size.y; | |
} | |
Lava.prototype.collide = function(state) { | |
return new State(state.level, state.actors, "lost"); | |
}; | |
Coin.prototype.collide = function(state) { | |
let filtered = state.actors.filter(a => a != this); | |
let status = state.status; | |
if (!filtered.some(a => a.type == "coin")) status = "won"; | |
return new State(state.level, filtered, status); | |
}; | |
Lava.prototype.update = function(time, state) { | |
let newPos = this.pos.plus(this.speed.times(time)); | |
if (!state.level.touches(newPos, this.size, "wall")) { | |
return new Lava(newPos, this.speed, this.reset); | |
} else if (this.reset) { | |
return new Lava(this.reset, this.speed, this.reset); | |
} else { | |
return new Lava(this.pos, this.speed.times(-1)); | |
} | |
}; | |
const wobbleSpeed = 8, wobbleDist = 0.07; | |
Coin.prototype.update = function(time) { | |
let wobble = this.wobble + time * wobbleSpeed; | |
let wobblePos = Math.sin(wobble) * wobbleDist; | |
return new Coin(this.basePos.plus(new Vec(0, wobblePos)), | |
this.basePos, wobble); | |
}; | |
const playerXSpeed = 7; | |
const gravity = 30; | |
const jumpSpeed = 17; | |
Player.prototype.update = function(time, state, keys) { | |
let xSpeed = 0; | |
if (keys.ArrowLeft) xSpeed -= playerXSpeed; | |
if (keys.ArrowRight) xSpeed += playerXSpeed; | |
let pos = this.pos; | |
let movedX = pos.plus(new Vec(xSpeed * time, 0)); | |
if (!state.level.touches(movedX, this.size, "wall")) { | |
pos = movedX; | |
} | |
let ySpeed = this.speed.y + time * gravity; | |
let movedY = pos.plus(new Vec(0, ySpeed * time)); | |
if (!state.level.touches(movedY, this.size, "wall")) { | |
pos = movedY; | |
} else if (keys.ArrowUp && ySpeed > 0) { | |
ySpeed = -jumpSpeed; | |
} else { | |
ySpeed = 0; | |
} | |
return new Player(pos, new Vec(xSpeed, ySpeed)); | |
}; | |
function trackKeys(keys) { | |
let down = Object.create(null); | |
function track(event) { | |
if (keys.includes(event.key)) { | |
down[event.key] = event.type == "keydown"; | |
event.preventDefault(); | |
} | |
} | |
window.addEventListener("keydown", track); | |
window.addEventListener("keyup", track); | |
return down; | |
} | |
const arrowKeys = | |
trackKeys(["ArrowLeft", "ArrowRight", "ArrowUp"]); | |
function runAnimation(frameFunc) { | |
let lastTime = null; | |
function frame(time) { | |
if (lastTime != null) { | |
let timeStep = Math.min(time - lastTime, 100) / 1000; | |
if (frameFunc(timeStep) === false) return; | |
} | |
lastTime = time; | |
requestAnimationFrame(frame); | |
} | |
requestAnimationFrame(frame); | |
} | |
function runLevel(level, Display) { | |
let display = new Display(document.body, level); | |
let state = State.start(level); | |
let ending = 1; | |
return new Promise(resolve => { | |
runAnimation(time => { | |
state = state.update(time, arrowKeys); | |
display.syncState(state); | |
if (state.status == "playing") { | |
return true; | |
} else if (ending > 0) { | |
ending -= time; | |
return true; | |
} else { | |
display.clear(); | |
resolve(state.status); | |
return false; | |
} | |
}); | |
}); | |
} | |
async function runGame(plans, Display) { | |
let lives = 3; | |
for (let level = 0; level < plans.length && lives > 0;) { | |
console.log(`level: ${level + 1}`, `lives: ${lives}`); | |
let status = await runLevel(new Level(plans[level]), | |
Display); | |
if (status == "won") { | |
level++; | |
} else { | |
lives--; | |
} | |
} | |
if(lives > 0){ | |
console.log("You've won!"); | |
} else { | |
console.log("You've lost!"); | |
} | |
} | |
runGame(GAME_LEVELS, DOMDisplay); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment