initial commit, walkable and idling character, first map with collision
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Thumbs.db*
|
||||||
|
|
||||||
|
*.afpub*
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
/tiles-paid
|
||||||
|
/modern-tiles_free
|
37
DirectionInput.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
class DirectionInput {
|
||||||
|
constructor() {
|
||||||
|
this.heldDirections = [];
|
||||||
|
|
||||||
|
this.map = {
|
||||||
|
"ArrowUp": "up",
|
||||||
|
"KeyW": "up",
|
||||||
|
"ArrowDown": "down",
|
||||||
|
"KeyS": "down",
|
||||||
|
"ArrowLeft": "left",
|
||||||
|
"KeyA": "left",
|
||||||
|
"ArrowRight": "right",
|
||||||
|
"KeyD": "right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get direction() {
|
||||||
|
return this.heldDirections[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
document.addEventListener("keydown", e => {
|
||||||
|
const dir = this.map[e.code];
|
||||||
|
if (dir && this.heldDirections.indexOf(dir) === -1) {
|
||||||
|
this.heldDirections.unshift(dir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keyup", e => {
|
||||||
|
const dir = this.map[e.code];
|
||||||
|
const index = this.heldDirections.indexOf(dir);
|
||||||
|
if (index > -1) {
|
||||||
|
this.heldDirections.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
22
GameObject.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
class GameObject {
|
||||||
|
constructor(config) {
|
||||||
|
this.isMounted = false;
|
||||||
|
this.x = config.x || 0;
|
||||||
|
this.y = config.y || 0;
|
||||||
|
this.direction = config.direction || "down";
|
||||||
|
this.sprite = new Sprite({
|
||||||
|
gameObject: this,
|
||||||
|
src: config.src || "/images/characters/people/hero.png",
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(map) {
|
||||||
|
this.isMounted = true;
|
||||||
|
map.addWall(this.x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
47
Overworld.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
class Overworld {
|
||||||
|
constructor(config) {
|
||||||
|
this.element = config.element;
|
||||||
|
this.canvas = this.element.querySelector(".game-canvas");
|
||||||
|
this.ctx = this.canvas.getContext("2d");
|
||||||
|
this.map = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startGameLoop() {
|
||||||
|
const step = () => {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
const cameraPerson = this.map.gameObjects.hero;
|
||||||
|
|
||||||
|
// update all objects
|
||||||
|
Object.values(this.map.gameObjects).forEach(object => {
|
||||||
|
object.update({
|
||||||
|
arrow: this.directionInput.direction,
|
||||||
|
map: this.map,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
this.map.drawLowerImage(this.ctx, cameraPerson);
|
||||||
|
|
||||||
|
Object.values(this.map.gameObjects).forEach(object => {
|
||||||
|
object.sprite.draw(this.ctx, cameraPerson);
|
||||||
|
})
|
||||||
|
|
||||||
|
this.map.drawUpperImage(this.ctx, cameraPerson);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
step();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
step();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.map = new OverworldMap(window.OverworldMaps.DemoRoom);
|
||||||
|
this.map.mountObjects();
|
||||||
|
|
||||||
|
this.directionInput = new DirectionInput();
|
||||||
|
this.directionInput.init();
|
||||||
|
|
||||||
|
this.startGameLoop();
|
||||||
|
}
|
||||||
|
}
|
123
OverworldMap.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
class OverworldMap {
|
||||||
|
constructor(config) {
|
||||||
|
this.gameObjects = config.gameObjects;
|
||||||
|
this.walls = config.walls || {};
|
||||||
|
|
||||||
|
this.lowerImage = new Image();
|
||||||
|
this.lowerImage.src = config.lowerSrc;
|
||||||
|
|
||||||
|
this.upperImage = new Image();
|
||||||
|
this.upperImage.src = config.upperSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLowerImage(ctx, cameraPerson) {
|
||||||
|
ctx.drawImage(this.lowerImage, utils.withGrid(10.5) - cameraPerson.x, utils.withGrid(6) - cameraPerson.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawUpperImage(ctx, cameraPerson) {
|
||||||
|
ctx.drawImage(this.upperImage, utils.withGrid(10.5) - cameraPerson.x, utils.withGrid(6) - cameraPerson.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSpaceTaken(currentX, currentY, direction) {
|
||||||
|
const {x, y} = utils.nextPosition(currentX, currentY, direction);
|
||||||
|
return this.walls[`${x},${y}`] || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mountObjects() {
|
||||||
|
Object.values(this.gameObjects).forEach(o => {
|
||||||
|
// todoL determine if this object should actually mount
|
||||||
|
|
||||||
|
o.mount(this);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addWall(x, y) {
|
||||||
|
this.walls[`${x},${y}`] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWall(x, y) {
|
||||||
|
delete this.walls[`${x},${y}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
moveWall(wasX, wasY, direction) {
|
||||||
|
this.removeWall(wasX, wasY);
|
||||||
|
const {x, y} = utils.nextPosition(wasX, wasY, direction);
|
||||||
|
this.addWall(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.OverworldMaps = {
|
||||||
|
DemoRoom: {
|
||||||
|
lowerSrc: "/images/maps/map-room-entrance.png",
|
||||||
|
upperSrc: "",
|
||||||
|
gameObjects: {
|
||||||
|
hero: new Person({
|
||||||
|
isPlayerControlled: true,
|
||||||
|
x: utils.withGrid(8),
|
||||||
|
y: utils.withGrid(2),
|
||||||
|
}),
|
||||||
|
// npc1: new Person({
|
||||||
|
// x: utils.withGrid(6),
|
||||||
|
// y: utils.withGrid(7),
|
||||||
|
// })
|
||||||
|
},
|
||||||
|
walls: {
|
||||||
|
// "16,16": true
|
||||||
|
[utils.asGridCoord(5,2)] : true,
|
||||||
|
[utils.asGridCoord(5,3)] : true,
|
||||||
|
[utils.asGridCoord(5,4)] : true,
|
||||||
|
[utils.asGridCoord(4,4)] : true,
|
||||||
|
[utils.asGridCoord(3,4)] : true,
|
||||||
|
[utils.asGridCoord(2,4)] : true,
|
||||||
|
[utils.asGridCoord(1,5)] : true,
|
||||||
|
[utils.asGridCoord(0,6)] : true,
|
||||||
|
[utils.asGridCoord(1,7)] : true,
|
||||||
|
[utils.asGridCoord(2,8)] : true,
|
||||||
|
[utils.asGridCoord(3,8)] : true,
|
||||||
|
[utils.asGridCoord(4,8)] : true,
|
||||||
|
[utils.asGridCoord(5,8)] : true,
|
||||||
|
[utils.asGridCoord(5,9)] : true,
|
||||||
|
[utils.asGridCoord(5,10)] : true,
|
||||||
|
[utils.asGridCoord(4,11)] : true,
|
||||||
|
[utils.asGridCoord(5,12)] : true,
|
||||||
|
[utils.asGridCoord(6,12)] : true,
|
||||||
|
[utils.asGridCoord(7,12)] : true,
|
||||||
|
[utils.asGridCoord(7,13)] : true,
|
||||||
|
[utils.asGridCoord(8,14)] : true,
|
||||||
|
[utils.asGridCoord(9,13)] : true,
|
||||||
|
[utils.asGridCoord(9,12)] : true,
|
||||||
|
[utils.asGridCoord(10,12)] : true,
|
||||||
|
[utils.asGridCoord(11,12)] : true,
|
||||||
|
[utils.asGridCoord(12,11)] : true,
|
||||||
|
[utils.asGridCoord(11,10)] : true,
|
||||||
|
[utils.asGridCoord(11,9)] : true,
|
||||||
|
[utils.asGridCoord(11,8)] : true,
|
||||||
|
[utils.asGridCoord(11,7)] : true,
|
||||||
|
[utils.asGridCoord(11,6)] : true,
|
||||||
|
[utils.asGridCoord(12,5)] : true,
|
||||||
|
[utils.asGridCoord(11,4)] : true,
|
||||||
|
[utils.asGridCoord(11,3)] : true,
|
||||||
|
[utils.asGridCoord(11,2)] : true,
|
||||||
|
[utils.asGridCoord(10,1)] : true,
|
||||||
|
[utils.asGridCoord(9,1)] : true,
|
||||||
|
[utils.asGridCoord(8,1)] : true,
|
||||||
|
[utils.asGridCoord(7,1)] : true,
|
||||||
|
[utils.asGridCoord(6,1)] : true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kitchen: {
|
||||||
|
lowerSrc: "/images/maps/room-builder.png",
|
||||||
|
upperSrc: "/images/maps/room-builder.png",
|
||||||
|
gameObjects: {
|
||||||
|
hero: new GameObject({
|
||||||
|
x: 2,
|
||||||
|
y: 3,
|
||||||
|
}),
|
||||||
|
npc1: new GameObject({
|
||||||
|
x: 3,
|
||||||
|
y: 6,
|
||||||
|
src: "/images/characters/people/hero-run.png"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
Person.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
class Person extends GameObject {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.movementProgressRemaining = 0;
|
||||||
|
|
||||||
|
this.isPlayerControlled = config.isPlayerControlled || false;
|
||||||
|
|
||||||
|
this.directionUpdate = {
|
||||||
|
"up": ["y", -1],
|
||||||
|
"down": ["y", 1],
|
||||||
|
"left": ["x", -1],
|
||||||
|
"right": ["x", 1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(state) {
|
||||||
|
if (this.movementProgressRemaining > 0) {
|
||||||
|
this.updatePosition();
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// more cases for starting to walk come here
|
||||||
|
|
||||||
|
// case: keyboard ready and arrow pressed
|
||||||
|
if (this.isPlayerControlled && state.arrow) {
|
||||||
|
this.startBehavior(state, {
|
||||||
|
type: "walk",
|
||||||
|
direction: state.arrow
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.updateSprite(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startBehavior(state, behavior) {
|
||||||
|
// set character direction to behavior
|
||||||
|
this.direction = behavior.direction;
|
||||||
|
if (behavior.type === "walk") {
|
||||||
|
// stop if space is not free
|
||||||
|
if (state.map.isSpaceTaken(this.x, this.y, this.direction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ready to walk
|
||||||
|
state.map.moveWall(this.x, this.y, this.direction);
|
||||||
|
this.movementProgressRemaining = 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition() {
|
||||||
|
const [property, change] = this.directionUpdate[this.direction];
|
||||||
|
this[property] += change;
|
||||||
|
this.movementProgressRemaining -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSprite() {
|
||||||
|
if (this.movementProgressRemaining > 0) {
|
||||||
|
this.sprite.setAnimation("walk-" + this.direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sprite.setAnimation("idle-" + this.direction);
|
||||||
|
}
|
||||||
|
}
|
102
Sprite.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
class Sprite {
|
||||||
|
constructor(config) {
|
||||||
|
// Setup image
|
||||||
|
this.image = new Image();
|
||||||
|
this.image.src = config.src;
|
||||||
|
this.image.onload = () => {
|
||||||
|
this.isLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
this.shadow = new Image();
|
||||||
|
this.useShadow = true;
|
||||||
|
if (this.useShadow) {
|
||||||
|
this.shadow.src = "/images/characters/shadow.png"
|
||||||
|
}
|
||||||
|
this.shadow.onload = () => {
|
||||||
|
this.isShadowLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure animation and initial state
|
||||||
|
this.animation = config.animation || {
|
||||||
|
"idle-up": [
|
||||||
|
[6, 1], [7, 1], [8, 1], [9, 1], [10, 1], [11, 1]
|
||||||
|
],
|
||||||
|
"idle-down": [
|
||||||
|
[18, 1], [19, 1], [20, 1], [21, 1], [22, 1], [23, 1]
|
||||||
|
],
|
||||||
|
"idle-left": [
|
||||||
|
[12, 1], [13, 1], [14, 1], [15, 1], [16, 1], [17, 1]
|
||||||
|
],
|
||||||
|
"idle-right": [
|
||||||
|
[0, 1], [1, 1], [2, 1], [3, 1], [4, 1], [5, 1]
|
||||||
|
],
|
||||||
|
"walk-up": [
|
||||||
|
[6, 2], [7, 2], [8, 2], [9, 2], [10, 2], [11, 2]
|
||||||
|
],
|
||||||
|
"walk-down": [
|
||||||
|
[18, 2], [19, 2], [20, 2], [21, 2], [22, 2], [23, 2]
|
||||||
|
],
|
||||||
|
"walk-left": [
|
||||||
|
[12, 2], [13, 2], [14, 2], [15, 2], [16, 2], [17, 2]
|
||||||
|
],
|
||||||
|
"walk-right": [
|
||||||
|
[0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [5, 2]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
this.currentAnimation = "idle-down"; // config.currentAnimation || "idle-down";
|
||||||
|
this.currentAnimationFrame = 0;
|
||||||
|
|
||||||
|
this.animationFrameLimit = config.animationFrameLimit || 6;
|
||||||
|
this.animationFrameProgress = this.animationFrameLimit;
|
||||||
|
|
||||||
|
// Reference game object
|
||||||
|
this.gameObject = config.gameObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
get frame() {
|
||||||
|
return this.animation[this.currentAnimation][this.currentAnimationFrame]
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnimation(key) {
|
||||||
|
if (this.currentAnimation !== key) {
|
||||||
|
this.currentAnimation = key;
|
||||||
|
this.currentAnimationFrame = 0;
|
||||||
|
this.animationFrameProgress = this.animationFrameLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnimationProgress() {
|
||||||
|
// downtick frame progress
|
||||||
|
if (this.animationFrameProgress > 0) {
|
||||||
|
this.animationFrameProgress -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the counter
|
||||||
|
this.animationFrameProgress = this.animationFrameLimit;
|
||||||
|
this.currentAnimationFrame += 1;
|
||||||
|
|
||||||
|
if (this.frame === undefined) {
|
||||||
|
this.currentAnimationFrame = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx, cameraPerson) {
|
||||||
|
const x = this.gameObject.x + utils.withGrid(10.5) - cameraPerson.x;
|
||||||
|
const y = this.gameObject.y - 4 + utils.withGrid(6) - cameraPerson.y;
|
||||||
|
|
||||||
|
this.isShadowLoaded && ctx.drawImage(this.shadow, x, y);
|
||||||
|
|
||||||
|
const [frameX, frameY] = this.frame;
|
||||||
|
|
||||||
|
this.isLoaded && ctx.drawImage(this.image,
|
||||||
|
frameX * 16, frameY * 32,
|
||||||
|
16, 32,
|
||||||
|
x, y,
|
||||||
|
16, 32
|
||||||
|
)
|
||||||
|
|
||||||
|
this.updateAnimationProgress();
|
||||||
|
}
|
||||||
|
}
|
BIN
images/characters/people/hero-idle.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
images/characters/people/hero-run.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
images/characters/people/hero.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
images/characters/shadow.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
images/maps/floor-test.jpg
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
images/maps/map-room-entrance.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
images/maps/room-builder.png
Normal file
After Width: | Height: | Size: 11 KiB |
23
index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<title>witchday</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<canvas class="game-canvas" width="352" height="198"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/utils.js"></script>
|
||||||
|
<script src="/DirectionInput.js"></script>
|
||||||
|
<script src="/Overworld.js"></script>
|
||||||
|
<script src="/GameObject.js"></script>
|
||||||
|
<script src="/Person.js"></script>
|
||||||
|
<script src="/Sprite.js"></script>
|
||||||
|
<script src="/OverworldMap.js"></script>
|
||||||
|
<script src="/init.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
init.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
(function () {
|
||||||
|
|
||||||
|
const overworld = new Overworld({
|
||||||
|
element: document.querySelector(".game-container")
|
||||||
|
});
|
||||||
|
overworld.init();
|
||||||
|
|
||||||
|
})();
|
BIN
map-room-entrance.jpg
Normal file
After Width: | Height: | Size: 113 KiB |
25
style.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
position: relative;
|
||||||
|
width: 352px;
|
||||||
|
height: 198px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
outline: 1px solid #fff;
|
||||||
|
|
||||||
|
transform: scale(2) translateY(30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
25
utils.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const utils = {
|
||||||
|
withGrid(n) {
|
||||||
|
return n * 16;
|
||||||
|
},
|
||||||
|
asGridCoord(x, y) {
|
||||||
|
return `${x*16},${y*16}`
|
||||||
|
},
|
||||||
|
nextPosition(initialX, initialY, direction) {
|
||||||
|
let x = initialX;
|
||||||
|
let y = initialY;
|
||||||
|
const size = 16;
|
||||||
|
|
||||||
|
if (direction === "left") {
|
||||||
|
x -= size;
|
||||||
|
} else if (direction === "right") {
|
||||||
|
x += size;
|
||||||
|
} else if (direction === "up") {
|
||||||
|
y -= size;
|
||||||
|
} else if (direction === "down") {
|
||||||
|
y += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {x, y};
|
||||||
|
}
|
||||||
|
}
|