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