initial commit, walkable and idling character, first map with collision

This commit is contained in:
Felix Baumgärtner 2024-07-30 20:56:24 +02:00
commit 6361a6c539
19 changed files with 481 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
Thumbs.db*
*.afpub*
*.zip
/tiles-paid
/modern-tiles_free

37
DirectionInput.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/maps/floor-test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

23
index.html Normal file
View 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
View File

@ -0,0 +1,8 @@
(function () {
const overworld = new Overworld({
element: document.querySelector(".game-container")
});
overworld.init();
})();

BIN
map-room-entrance.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

25
style.css Normal file
View 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
View 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};
}
}