import * as Phaser from "phaser"; import { Scene, Tilemaps, GameObjects, Physics, Math as Mathph } from "phaser"; import { Player } from "../../classes/player"; import { NPC } from "../../classes/npc"; import { DIRECTION } from "../../utils"; import { TextBox, Click, } from "../../phaser3-rex-plugins/templates/ui/ui-components"; import UIPlugin from "../../phaser3-rex-plugins/templates/ui/ui-plugin"; import BoardPlugin from "../../phaser3-rex-plugins/plugins/board-plugin"; import { PathFinder } from "../../phaser3-rex-plugins/plugins/board-components"; import { TileXYType } from "../../phaser3-rex-plugins/plugins/board/types/Position"; import { shuffle } from "../../utils"; import { COLOR_DARK, COLOR_LIGHT, COLOR_PRIMARY } from "../../constants"; export class TownScene extends Scene { private timeFrame: number = 0; private isQuerying: boolean = false; private map: Tilemaps.Tilemap; private tileset: Tilemaps.Tileset; private groundLayer: Tilemaps.TilemapLayer; private wallLayer: Tilemaps.TilemapLayer; private flowerLayer: Tilemaps.TilemapLayer; private treeLayer: Tilemaps.TilemapLayer; private houseLayer: Tilemaps.TilemapLayer; private player: Player; private npcGroup: GameObjects.Group; private keySpace: Phaser.Input.Keyboard.Key; private keyEnter: Phaser.Input.Keyboard.Key; public rexUI: UIPlugin; public rexBoard: BoardPlugin; private board: BoardPlugin.Board; private pathFinder: PathFinder; constructor() { super("town-scene"); } create(): void { this.keySpace = this.input.keyboard!.addKey("SPACE"); this.keyEnter = this.input.keyboard!.addKey("ENTER"); this.initMap(); this.initSprite(); this.initCamera(); // this.add.grid(0, 0, 1024, 1024, 16, 16, 0x000000).setAlpha(0.1); } update(time, delta): void { this.timeFrame += delta; this.player.update(); this.npcGroup.getChildren().forEach(function (npc) { (npc as NPC).update(); }); if (this.timeFrame > 5000) { if (!this.isQuerying) { this.isQuerying = true; var allNpcs = this.npcGroup.getChildren(); var shouldUpdate = []; for (let i = 0; i < this.npcGroup.getLength(); i++) { // for (let i = 0; i < 1; i++) { if ( !(allNpcs[i] as NPC).isMoving() && !(allNpcs[i] as NPC).isTalking() ) { shouldUpdate.push(i); } } fetch("http://127.0.0.1:10002/make_decision", { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "same-origin", body: JSON.stringify({ agent_ids: shouldUpdate, }), }).then((response) => { response.json().then((data) => { this.npcGroup.getChildren().forEach(function (npc) { (npc as NPC).destroyTextBox(); }); for (let i = 0; i < data.length; i++) { var npc = allNpcs[shouldUpdate[i]] as NPC; if (data[i].content == "") continue; var content = JSON.parse(data[i].content); switch (content.action) { case "MoveTo": var tile = this.getRandomTileAtLocation(content.to); if (tile == undefined) break; npc.destroyTextBox(); this.moveNPC(shouldUpdate[i], tile, undefined, content.to); break; case "Speak": var ret = this.getNPCNeighbor(content.to); var tile = ret[0]; var finalDirection = ret[1]; var listener = ret[2]; if (tile == undefined) break; this.moveNPC( shouldUpdate[i], tile, finalDirection, undefined, listener ); npc.setTextBox(content.text); break; default: npc.setTextBox("[" + content.action + "]"); break; } } this.isQuerying = false; }); }); } this.timeFrame = 0; } } initMap(): void { this.map = this.make.tilemap({ key: "town", tileWidth: 16, tileHeight: 16, }); this.tileset = this.map.addTilesetImage("town", "tiles")!; this.groundLayer = this.map.createLayer("ground", this.tileset, 0, 0)!; this.wallLayer = this.map.createLayer("wall", this.tileset, 0, 0)!; this.flowerLayer = this.map.createLayer("flower", this.tileset, 0, 0)!; this.treeLayer = this.map.createLayer("tree", this.tileset, 0, 0)!; this.houseLayer = this.map.createLayer("house", this.tileset, 0, 0)!; this.wallLayer.setCollisionByProperty({ collides: true }); this.treeLayer.setCollisionByProperty({ collides: true }); this.houseLayer.setCollisionByProperty({ collides: true }); this.board = this.rexBoard.createBoardFromTilemap(this.map); this.board.getAllChess().forEach((chess) => { var collide = ["wall", "tree", "house"].includes(chess.layer.name); if (collide && chess.index != -1) { chess.rexChess.setBlocker(); } }); this.pathFinder = this.rexBoard.add.pathFinder({ occupiedTest: true, blockerTest: true, pathMode: "straight", cacheCost: true, }); } initSprite(): void { // NPC this.npcGroup = this.add.group(); var npcPoints = this.map.filterObjects("npcs", (npc) => { return npc.type === "npc"; }); for (let i = 0; i < npcPoints.length; i++) { var npcPoint = this.map.findObject("npcs", (npc) => { for (let j = 0; j < npc.properties.length; j++) { if (npc.properties[j].name === "id") { return npc.properties[j].value === i; } } }); var tileXY = this.board.worldXYToTileXY(npcPoint.x, npcPoint.y); var npc = new NPC( this, this.board, npcPoint.x, npcPoint.y, npcPoint.name, npcPoint.properties[0].value ); this.board.addChess(npc, tileXY.x, tileXY.y, 0, true); this.physics.add.collider(npc, this.npcGroup); this.npcGroup.add(npc); } this.physics.add.collider(this.npcGroup, this.wallLayer); this.physics.add.collider(this.npcGroup, this.treeLayer); this.physics.add.collider(this.npcGroup, this.houseLayer); // this.physics.add.collider(this.npcGroup, this.npcGroup); // Player this.player = new Player(this, 288, 240); this.physics.add.collider(this.player, this.wallLayer); this.physics.add.collider(this.player, this.treeLayer); this.physics.add.collider(this.player, this.houseLayer); this.physics.add.collider( this.player, this.npcGroup, (player: Player, npc: NPC) => { npc.pauseMoving(); var checkResumeWalk = this.time.addEvent({ delay: 1000, callback: () => { const nearbyDistance = 1.1 * Math.max(player.width, player.height); var distance = Mathph.Distance.Between( player.x, player.y, npc.x, npc.y ); if (distance > nearbyDistance) { npc.resumeMoving(); checkResumeWalk.destroy(); } }, }); } ); this.keySpace.on("up", () => { var ret = getNearbyNPC(this.player, this.npcGroup); var npc = ret[0]; if (npc) { npc = npc as NPC; (npc as NPC).changeDirection(ret[1]); (npc as NPC).setTalking(true); this.createInputBox(npc); } }); // this.keyEnter.on("up", () => {}); this.physics.world.setBounds( 0, 0, this.groundLayer.width + this.player.width, this.groundLayer.height ); } initCamera(): void { this.cameras.main.setSize(this.game.scale.width, this.game.scale.height); this.cameras.main.setBounds( 0, 0, this.groundLayer.width, this.groundLayer.height ); this.cameras.main.startFollow(this.player, true, 0.09, 0.09); this.cameras.main.setZoom(4); } disableKeyboard(): void { this.input.keyboard.manager.enabled = false; } enableKeyboard(): void { this.input.keyboard.manager.enabled = true; } createInputBox(npc: Physics.Arcade.Sprite) { this.disableKeyboard(); var upperLeftCorner = this.cameras.main.getWorldPoint( this.cameras.main.width * 0.2, this.cameras.main.height * 0.3 ); var x = upperLeftCorner.x; var y = upperLeftCorner.y; var width = this.cameras.main.width; var height = this.cameras.main.height; var scale = this.cameras.main.zoom; var inputText = this.rexUI.add .inputText({ x: x, y: y, width: width * 0.6, height: height * 0.3, type: "textarea", text: "", color: "#ffffff", border: 2, backgroundColor: "#" + COLOR_DARK.toString(16), borderColor: "#" + COLOR_LIGHT.toString(16), }) .setOrigin(0) .setScale(1 / scale, 1 / scale) .setFocus() .setAlpha(0.8); const self = this; var submitBtn = this.rexUI.add .label({ x: x, y: y + inputText.height / scale + 5, background: this.rexUI.add .roundRectangle(0, 0, 2, 2, 20, COLOR_PRIMARY) .setStrokeStyle(2, COLOR_LIGHT), text: this.add.text(0, 0, "Submit"), space: { left: 10, right: 10, top: 10, bottom: 10, }, }) .setOrigin(0) .setScale(1 / scale, 1 / scale) .layout(); var cancelBtn = this.rexUI.add .label({ x: x + submitBtn.width / scale + 5, y: y + inputText.height / scale + 5, background: this.rexUI.add .roundRectangle(0, 0, 2, 2, 20, COLOR_PRIMARY) .setStrokeStyle(2, COLOR_LIGHT), text: this.add.text(0, 0, "Cancel"), space: { left: 10, right: 10, top: 10, bottom: 10, }, }) .setOrigin(0) .setScale(1 / scale, 1 / scale) .layout(); submitBtn.onClick(function ( click: Click, gameObject: Phaser.GameObjects.GameObject, pointer: Phaser.Input.Pointer, event: Phaser.Types.Input.EventData ) { let text = inputText.text; inputText.destroy(); gameObject.destroy(); cancelBtn.destroy(); self.submitPrompt(text, npc); }); cancelBtn.onClick(function ( click: Click, gameObject: Phaser.GameObjects.GameObject, pointer: Phaser.Input.Pointer, event: Phaser.Types.Input.EventData ) { inputText.destroy(); gameObject.destroy(); submitBtn.destroy(); self.enableKeyboard(); }); } submitPrompt(prompt: string, npc: Physics.Arcade.Sprite) { var waitingBox = this.createTextBox().start( "Waiting for the response...", 200 ); var timer = this.time.addEvent({ delay: 6000, // ms callback: () => { waitingBox.destroy(); waitingBox = this.createTextBox().start( "Waiting for the response...", 200 ); }, loop: true, }); fetch("http://127.0.0.1:10002/chat", { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "same-origin", body: JSON.stringify({ content: prompt, sender: "Brendan", receiver_id: (npc as NPC).id, receiver: (npc as NPC).name, }), }).then((response) => { response.json().then((data) => { // console.log(data); timer.destroy(); waitingBox.destroy(); var content = JSON.parse(data.content); var responseBox = this.createTextBox() .start(content.text, 25) .on("complete", () => { this.enableKeyboard(); this.input.keyboard.on("keydown", () => { responseBox.destroy(); this.input.keyboard.off("keydown"); (npc as NPC).setTalking(false); }); }); }); }); } createTextBox(): TextBox { var upperLeftCorner = this.cameras.main.getWorldPoint( this.cameras.main.width * 0.1, this.cameras.main.height * 0.8 ); var x = upperLeftCorner.x; var y = upperLeftCorner.y; var width = this.cameras.main.width * 0.8; var height = this.cameras.main.height * 0.15; var textBox = this.rexUI.add .textBox({ x: x, y: y, background: this.rexUI.add.roundRectangle( 0, 0, 2, 2, 20, COLOR_PRIMARY ), text: this.add .text(0, 0, "", { fixedWidth: width, wordWrap: { width: width, }, }) .setFixedSize(width, height), space: { left: 20, right: 20, top: 20, bottom: 20, icon: 10, text: 10, }, }) .setScale(0.25, 0.25) .setOrigin(0) .setDepth(Number.MAX_SAFE_INTEGER) .layout(); return textBox; } getRandomTileAtLocation(location_name: string): TileXYType { var location = this.map.findObject("location", function (object) { return object.name == location_name; }); var x = location.x; var y = location.y; var width = location.width; var height = location.height; var cnt = 0; debugger; do { if (cnt > 10) { console.log("Failed to find a random tile"); return null; } var worldX = Math.floor(Math.random() * width) + x; var worldY = Math.floor(Math.random() * height) + y; var tile = this.board.worldXYToTileXY(worldX, worldY); cnt++; } while ( this.board.hasBlocker(tile.x, tile.y) || // has wall this.board.tileXYToChessArray(tile.x, tile.y).length != this.map.layers.length // has npc ); return tile; } getNPCNeighbor(npc_name: string): [TileXYType, number, NPC] { var npc = this.npcGroup.getChildren().find((npc) => { return (npc as NPC).name == npc_name; }) as NPC; var npcTile = this.board.worldXYToTileXY(npc.x, npc.y); var directions = [ [-1, 0], [1, 0], [0, -1], [0, 1], ]; var order = shuffle([0, 1, 2, 3]); var tileX = undefined; var tileY = undefined; for (let i = 0; i < 4; i++) { var direction = directions[order[i]]; var tmpX = npcTile.x + direction[0]; var tmpY = npcTile.y + direction[1]; if ( !this.board.hasBlocker(tmpX, tmpY) && // no wall this.board.tileXYToChessArray(tmpX, tmpY).length == this.map.layers.length // no npc) ) { tileX = tmpX; tileY = tmpY; break; } } var finalDirection = DIRECTION.DOWN; if (direction[0] == 0 && direction[1] == 1) { finalDirection = DIRECTION.UP; } else if (direction[0] == 0 && direction[1] == -1) { finalDirection = DIRECTION.DOWN; } else if (direction[0] == 1 && direction[1] == 0) { finalDirection = DIRECTION.LEFT; } else if (direction[0] == -1 && direction[1] == 0) { finalDirection = DIRECTION.RIGHT; } return [{ x: tileX, y: tileY }, finalDirection, npc]; } moveNPC( npcId: number, tile, finalDirection: number = undefined, targetLocation: string = undefined, targetNPC: NPC = undefined ): void { var npc = this.npcGroup.getChildren()[npcId] as NPC; var npc_chess = this.board.worldXYToChess(npc.x, npc.y); this.pathFinder.setChess(npc_chess); // var tmp = this.board.chessToTileXYZ(npc_chess); var path = this.pathFinder.findPath({ x: tile.x, y: tile.y, } as TileXYType); npc.setTargetNPC(targetNPC); npc.moveAlongPath(path, finalDirection, targetLocation); } } function getNearbyNPC( player: Physics.Arcade.Sprite, npcGroup: GameObjects.Group ): [Physics.Arcade.Sprite | null, number] { var nearbyObject: Physics.Arcade.Sprite | null = null; // Not rigorous. Just a rough estimation. Requires that the npcs have // similar width and height to player. const nearbyDistance = 1.1 * Math.max(player.width, player.height); var direction = 0; npcGroup.getChildren().forEach(function (object) { var _object = object as Physics.Arcade.Sprite; const distance = Mathph.Distance.Between( player.x, player.y, _object.x, _object.y ); if (distance <= nearbyDistance) { nearbyObject = _object; var x_ratio = (player.x - _object.x) / _object.width; var y_ratio = (player.y - _object.y) / _object.height; if (Math.abs(x_ratio) > Math.abs(y_ratio)) { if (x_ratio > 0) { direction = DIRECTION.RIGHT; } else { direction = DIRECTION.LEFT; } } else { if (y_ratio > 0) { direction = DIRECTION.DOWN; } else { direction = DIRECTION.UP; } } } }); return [nearbyObject, direction]; }