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};
|
||||
}
|
||||
}
|