From d4a7de346d5067e4b9bb73578b553c5f71b5edac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20Baumg=C3=A4rtner?= <mail@felixbaumgaertner.de>
Date: Fri, 2 Aug 2024 16:01:51 +0200
Subject: [PATCH] added behaviors and cutscene logic like here:
 https://www.youtube.com/watch?v=e144CXGy2mc&list=PLcjhmZ8oLT0r9dSiIK6RB_PuBWlG1KSq_&index=9

---
 GameObject.js     | 29 +++++++++++++++++++++++++
 Overworld.js      | 11 +++++++++-
 OverworldEvent.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++
 OverworldMap.js   | 49 ++++++++++++++++++++++++++++++++++++------
 Person.js         | 20 +++++++++++++++++-
 index.html        |  1 +
 utils.js          |  7 ++++++
 7 files changed, 162 insertions(+), 9 deletions(-)
 create mode 100644 OverworldEvent.js

diff --git a/GameObject.js b/GameObject.js
index 16cc04e..e3d7d87 100644
--- a/GameObject.js
+++ b/GameObject.js
@@ -1,5 +1,6 @@
 class GameObject {
 	constructor(config) {
+		this.id = null;
 		this.isMounted = false;
 		this.x = config.x || 0;
 		this.y = config.y || 0;
@@ -9,14 +10,42 @@ class GameObject {
 			src: config.src || "/images/characters/people/hero.png",
 
 		});
+
+		this.behaviorLoop = config.behaviorLoop || [];
+		this.behaviorLoopIndex = 0;
 	}
 
 	mount(map) {
 		this.isMounted = true;
 		map.addWall(this.x, this.y);
+
+		// when behavior kick off after delay
+		setTimeout(() => {
+			this.doBehaviorEvent(map);
+		}, 10);
 	}
 
 	update() {
 
 	}
+
+	async doBehaviorEvent(map) {
+		// stop if cutscene is playing
+		if (map.isCutscenePlaying || this.behaviorLoop.length === 0) {
+			return;
+		}
+
+		let eventConfig = this.behaviorLoop[this.behaviorLoopIndex];
+		eventConfig.who = this.id;
+
+		const eventHandler = new OverworldEvent({ map, event: eventConfig });
+		await eventHandler.init();
+
+		this.behaviorLoopIndex += 1;
+		if (this.behaviorLoopIndex === this.behaviorLoop.length) {
+			this.behaviorLoopIndex = 0;
+		}
+
+		this.doBehaviorEvent(map);
+	}
 }
\ No newline at end of file
diff --git a/Overworld.js b/Overworld.js
index 2a253d9..489d21a 100644
--- a/Overworld.js
+++ b/Overworld.js
@@ -22,7 +22,9 @@ class Overworld {
 
 			this.map.drawLowerImage(this.ctx, cameraPerson);
 
-			Object.values(this.map.gameObjects).forEach(object => {
+			Object.values(this.map.gameObjects).sort((a,b) => {
+				return a.y - b.y;
+			}).forEach(object => {
 				object.sprite.draw(this.ctx, cameraPerson);
 			})
 
@@ -43,5 +45,12 @@ class Overworld {
 		this.directionInput.init();
 
 		this.startGameLoop();
+
+		this.map.startCutscene([
+			{ who: "hero", type: "walk", direction: "down" },
+			{ who: "hero", type: "walk", direction: "down" },
+			{ who: "hero", type: "walk", direction: "down" },
+			{ who: "npc1", type: "walk", direction: "right" },
+		])
 	}
 }
\ No newline at end of file
diff --git a/OverworldEvent.js b/OverworldEvent.js
new file mode 100644
index 0000000..7e98057
--- /dev/null
+++ b/OverworldEvent.js
@@ -0,0 +1,54 @@
+class OverworldEvent {
+	constructor({map, event}) {
+		this.map = map;
+		this.event = event;
+	}
+
+	stand(resolve) {
+		const who = this.map.gameObjects[ this.event.who ];
+		who.startBehavior({
+			map: this.map
+		}, {
+			type: "stand",
+			direction: this.event.direction,
+			time: this.event.time
+		})
+
+		// handler to complete when correct person is done walking
+		const completeHandler = e => {
+			if (e.detail.whoId === this.event.who) {
+				document.removeEventListener("PersonStandComplete", completeHandler);
+				resolve();
+			}
+		}
+
+		document.addEventListener("PersonStandComplete", completeHandler)
+	}
+
+	walk(resolve) {
+		const who = this.map.gameObjects[ this.event.who ];
+		who.startBehavior({
+			map: this.map
+		}, {
+			type: "walk",
+			direction: this.event.direction,
+			retry: true
+		})
+
+		// handler to complete when correct person is done walking
+		const completeHandler = e => {
+			if (e.detail.whoId === this.event.who) {
+				document.removeEventListener("PersonWalkingComplete", completeHandler);
+				resolve();
+			}
+		}
+
+		document.addEventListener("PersonWalkingComplete", completeHandler)
+	}
+
+	init() {
+		return new Promise(resolve => {
+			this[this.event.type](resolve)
+		})
+	}
+}
\ No newline at end of file
diff --git a/OverworldMap.js b/OverworldMap.js
index 7c1ccb3..21a873b 100644
--- a/OverworldMap.js
+++ b/OverworldMap.js
@@ -8,6 +8,8 @@ class OverworldMap {
 
 		this.upperImage = new Image();
 		this.upperImage.src = config.upperSrc;
+
+		this.isCutscenePlaying = false;
 	}
 
 	drawLowerImage(ctx, cameraPerson) {
@@ -24,13 +26,29 @@ class OverworldMap {
 	}
 
 	mountObjects() {
-		Object.values(this.gameObjects).forEach(o => {
-			// todoL determine if this object should actually mount
+		Object.keys(this.gameObjects).forEach(key => {
+			let object = this.gameObjects[key];
+			object.id = key;
 
-			o.mount(this);
+
+			object.mount(this);
 		})
 	}
 
+	async startCutscene(events) {
+		this.isCutscenePlaying = true;
+
+		for (let i = 0; i < events.length; i++) {
+			const eventHandler = new OverworldEvent({
+				event: events[i],
+				map: this,
+			})
+			await eventHandler.init();
+		}
+
+		this.isCutscenePlaying = false;
+	}
+
 	addWall(x, y) {
 		this.walls[`${x},${y}`] = true;
 	}
@@ -56,10 +74,27 @@ window.OverworldMaps = {
 				x: utils.withGrid(8),
 				y: utils.withGrid(2),
 			}),
-			// npc1: new Person({
-			// 	x: utils.withGrid(6),
-			// 	y: utils.withGrid(7),
-			// })
+			npc1: new Person({
+				x: utils.withGrid(6),
+				y: utils.withGrid(5),
+				behaviorLoop: [
+					{ type: "stand", direction: "down", time: 800 },
+					{ type: "stand", direction: "right", time: 300 },
+					{ type: "stand", direction: "down", time: 400 },
+					{ type: "stand", direction: "up", time: 700 },
+				]
+			}),
+			npc2: new Person({
+				x: utils.withGrid(8),
+				y: utils.withGrid(9),
+				behaviorLoop: [
+					{ type: "walk", direction: "left" },
+					{ type: "stand", direction: "up", time: 800 },
+					{ type: "walk", direction: "up" },
+					{ type: "walk", direction: "right" },
+					{ type: "walk", direction: "down" },
+				]
+			})
 		},
 		walls: {
 			// "16,16": true
diff --git a/Person.js b/Person.js
index b1b55a5..de86efd 100644
--- a/Person.js
+++ b/Person.js
@@ -21,7 +21,7 @@ class Person extends GameObject {
 			// more cases for starting to walk come here
 
 			// case: keyboard ready and arrow pressed
-			if (this.isPlayerControlled && state.arrow) {
+			if (state.map.isCutscenePlaying == false && this.isPlayerControlled && state.arrow) {
 				this.startBehavior(state, {
 					type: "walk",
 					direction: state.arrow
@@ -37,12 +37,24 @@ class Person extends GameObject {
 		if (behavior.type === "walk") {
 			// stop if space is not free
 			if (state.map.isSpaceTaken(this.x, this.y, this.direction)) {
+				behavior.retry && setTimeout(() => {
+					this.startBehavior(state, behavior);
+				}, 10);
 				return;
 			}
 
 			// ready to walk
 			state.map.moveWall(this.x, this.y, this.direction);
 			this.movementProgressRemaining = 16;
+			this.updateSprite(state);
+		}
+
+		if (behavior.type = "stand") {
+			setTimeout(() => {
+				utils.emitEvent("PersonStandComplete", {
+					whoId: this.id
+				})
+			}, behavior.time);
 		}
 	}
 
@@ -50,6 +62,12 @@ class Person extends GameObject {
 		const [property, change] = this.directionUpdate[this.direction];
 		this[property] += change;
 		this.movementProgressRemaining -= 1;
+
+		if (this.movementProgressRemaining === 0) {
+			utils.emitEvent("PersonWalkingComplete", {
+				whoId: this.id
+			})
+		}
 	}
 
 	updateSprite() {
diff --git a/index.html b/index.html
index e629de6..313bb4b 100644
--- a/index.html
+++ b/index.html
@@ -18,6 +18,7 @@
 	<script src="/Person.js"></script>
 	<script src="/Sprite.js"></script>
 	<script src="/OverworldMap.js"></script>
+	<script src="/OverworldEvent.js"></script>
 	<script src="/init.js"></script>
 </body>
 </html>
\ No newline at end of file
diff --git a/utils.js b/utils.js
index 24568b1..d7ba581 100644
--- a/utils.js
+++ b/utils.js
@@ -21,5 +21,12 @@ const utils = {
 		}
 
 		return {x, y};
+	},
+	emitEvent(name, detail) {
+		const event = new CustomEvent(name, {
+			detail
+		});
+		document.dispatchEvent(event);
+
 	}
 }
\ No newline at end of file