commit 6361a6c539ef39c2233d127e89a1a24c44d1e3ff Author: Felix Baumgärtner Date: Tue Jul 30 20:56:24 2024 +0200 initial commit, walkable and idling character, first map with collision diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66982ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +Thumbs.db* + +*.afpub* +*.zip + +/tiles-paid +/modern-tiles_free diff --git a/DirectionInput.js b/DirectionInput.js new file mode 100644 index 0000000..fa2bf00 --- /dev/null +++ b/DirectionInput.js @@ -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); + } + }); + } +} \ No newline at end of file diff --git a/GameObject.js b/GameObject.js new file mode 100644 index 0000000..16cc04e --- /dev/null +++ b/GameObject.js @@ -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() { + + } +} \ No newline at end of file diff --git a/Overworld.js b/Overworld.js new file mode 100644 index 0000000..2a253d9 --- /dev/null +++ b/Overworld.js @@ -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(); + } +} \ No newline at end of file diff --git a/OverworldMap.js b/OverworldMap.js new file mode 100644 index 0000000..7c1ccb3 --- /dev/null +++ b/OverworldMap.js @@ -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" + }) + } + } +} \ No newline at end of file diff --git a/Person.js b/Person.js new file mode 100644 index 0000000..b1b55a5 --- /dev/null +++ b/Person.js @@ -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); + } +} \ No newline at end of file diff --git a/Sprite.js b/Sprite.js new file mode 100644 index 0000000..d5704fe --- /dev/null +++ b/Sprite.js @@ -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(); + } +} \ No newline at end of file diff --git a/images/characters/people/hero-idle.png b/images/characters/people/hero-idle.png new file mode 100644 index 0000000..7c53b67 Binary files /dev/null and b/images/characters/people/hero-idle.png differ diff --git a/images/characters/people/hero-run.png b/images/characters/people/hero-run.png new file mode 100644 index 0000000..e422c6f Binary files /dev/null and b/images/characters/people/hero-run.png differ diff --git a/images/characters/people/hero.png b/images/characters/people/hero.png new file mode 100644 index 0000000..d26ccd7 Binary files /dev/null and b/images/characters/people/hero.png differ diff --git a/images/characters/shadow.png b/images/characters/shadow.png new file mode 100644 index 0000000..0cc3895 Binary files /dev/null and b/images/characters/shadow.png differ diff --git a/images/maps/floor-test.jpg b/images/maps/floor-test.jpg new file mode 100644 index 0000000..91e0f25 Binary files /dev/null and b/images/maps/floor-test.jpg differ diff --git a/images/maps/map-room-entrance.png b/images/maps/map-room-entrance.png new file mode 100644 index 0000000..6ce4c89 Binary files /dev/null and b/images/maps/map-room-entrance.png differ diff --git a/images/maps/room-builder.png b/images/maps/room-builder.png new file mode 100644 index 0000000..026bca5 Binary files /dev/null and b/images/maps/room-builder.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..e629de6 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + witchday + + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/init.js b/init.js new file mode 100644 index 0000000..bb91f0e --- /dev/null +++ b/init.js @@ -0,0 +1,8 @@ +(function () { + + const overworld = new Overworld({ + element: document.querySelector(".game-container") + }); + overworld.init(); + +})(); \ No newline at end of file diff --git a/map-room-entrance.jpg b/map-room-entrance.jpg new file mode 100644 index 0000000..91e0f25 Binary files /dev/null and b/map-room-entrance.jpg differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..9ca09f1 --- /dev/null +++ b/style.css @@ -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; +} \ No newline at end of file diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..24568b1 --- /dev/null +++ b/utils.js @@ -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}; + } +} \ No newline at end of file