diff --git a/front/.gitignore b/front/.gitignore index 1842041a..d76848c8 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -6,6 +6,7 @@ /dist/main.*.css.map /dist/tests/ /yarn-error.log +/package-lock.json /dist/webpack.config.js /dist/webpack.config.js.map /dist/src diff --git a/front/package.json b/front/package.json index 935c254f..4a4e78f6 100644 --- a/front/package.json +++ b/front/package.json @@ -48,6 +48,7 @@ "axios": "^0.21.2", "cross-env": "^7.0.3", "deep-copy-ts": "^0.5.0", + "easystarjs": "^0.4.4", "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", "phaser": "^3.54.0", diff --git a/front/src/Interfaces/UserInputHandlerInterface.ts b/front/src/Interfaces/UserInputHandlerInterface.ts new file mode 100644 index 00000000..cf7b2f1c --- /dev/null +++ b/front/src/Interfaces/UserInputHandlerInterface.ts @@ -0,0 +1,12 @@ +export interface UserInputHandlerInterface { + handleMouseWheelEvent: ( + pointer: Phaser.Input.Pointer, + gameObjects: Phaser.GameObjects.GameObject[], + deltaX: number, + deltaY: number, + deltaZ: number + ) => void; + handlePointerUpEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void; + handlePointerDownEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void; + handleSpaceKeyUpEvent: (event: Event) => Event; +} diff --git a/front/src/Phaser/Components/MobileJoystick.ts b/front/src/Phaser/Components/MobileJoystick.ts index 9c14cb8e..27819613 100644 --- a/front/src/Phaser/Components/MobileJoystick.ts +++ b/front/src/Phaser/Components/MobileJoystick.ts @@ -40,30 +40,22 @@ export class MobileJoystick extends VirtualJoystick { this.visible = false; this.enable = false; - this.scene.input.on("pointerdown", (pointer: Phaser.Input.Pointer) => { - if (!pointer.wasTouch) { - return; - } - - // Let's only display the joystick if there is one finger on the screen - if ((pointer.event as TouchEvent).touches.length === 1) { - this.x = pointer.x; - this.y = pointer.y; - this.visible = true; - this.enable = true; - } else { - this.visible = false; - this.enable = false; - } - }); - this.scene.input.on("pointerup", () => { - this.visible = false; - this.enable = false; - }); this.resizeCallback = this.resize.bind(this); this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback); } + public showAt(x: number, y: number): void { + this.x = x; + this.y = y; + this.visible = true; + this.enable = true; + } + + public hide(): void { + this.visible = false; + this.enable = false; + } + public resize() { this.base.setDisplaySize(this.getDisplaySizeByElement(baseSize), this.getDisplaySizeByElement(baseSize)); this.thumb.setDisplaySize(this.getDisplaySizeByElement(thumbSize), this.getDisplaySizeByElement(thumbSize)); diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 5778c065..fc16110f 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,10 +1,4 @@ -import type { - ITiledMap, - ITiledMapLayer, - ITiledMapObject, - ITiledMapObjectLayer, - ITiledMapProperty, -} from "../Map/ITiledMap"; +import type { ITiledMap, ITiledMapLayer, ITiledMapObject, ITiledMapProperty } from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; @@ -120,8 +114,24 @@ export class GameMap { return []; } - private getLayersByKey(key: number): Array { - return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0); + public getCollisionsGrid(): number[][] { + const grid: number[][] = []; + for (let y = 0; y < this.map.height; y += 1) { + const row: number[] = []; + for (let x = 0; x < this.map.width; x += 1) { + row.push(this.isCollidingAt(x, y) ? 1 : 0); + } + grid.push(row); + } + return grid; + } + + public getTileDimensions(): { width: number; height: number } { + return { width: this.map.tilewidth, height: this.map.tileheight }; + } + + public getTileIndexAt(x: number, y: number): { x: number; y: number } { + return { x: Math.floor(x / this.map.tilewidth), y: Math.floor(y / this.map.tileheight) }; } /** @@ -149,6 +159,151 @@ export class GameMap { this.triggerLayersChange(); } + public getCurrentProperties(): Map { + return this.lastProperties; + } + + public getMap(): ITiledMap { + return this.map; + } + + /** + * Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on. + */ + public onPropertyChange(propName: string, callback: PropertyChangeCallback) { + let callbacksArray = this.propertiesChangeCallbacks.get(propName); + if (callbacksArray === undefined) { + callbacksArray = new Array(); + this.propertiesChangeCallbacks.set(propName, callbacksArray); + } + callbacksArray.push(callback); + } + + /** + * Registers a callback called when the user moves inside another layer. + */ + public onEnterLayer(callback: layerChangeCallback) { + this.enterLayerCallbacks.push(callback); + } + + /** + * Registers a callback called when the user moves outside another layer. + */ + public onLeaveLayer(callback: layerChangeCallback) { + this.leaveLayerCallbacks.push(callback); + } + + /** + * Registers a callback called when the user moves inside another zone. + */ + public onEnterZone(callback: zoneChangeCallback) { + this.enterZoneCallbacks.push(callback); + } + + /** + * Registers a callback called when the user moves outside another zone. + */ + public onLeaveZone(callback: zoneChangeCallback) { + this.leaveZoneCallbacks.push(callback); + } + + public findLayer(layerName: string): ITiledMapLayer | undefined { + return this.flatLayers.find((layer) => layer.name === layerName); + } + + public findPhaserLayer(layerName: string): TilemapLayer | undefined { + return this.phaserLayers.find((layer) => layer.layer.name === layerName); + } + + public findPhaserLayers(groupName: string): TilemapLayer[] { + return this.phaserLayers.filter((l) => l.layer.name.includes(groupName)); + } + + public addTerrain(terrain: Phaser.Tilemaps.Tileset): void { + for (const phaserLayer of this.phaserLayers) { + phaserLayer.tileset.push(terrain); + } + } + + public putTile(tile: string | number | null, x: number, y: number, layer: string): void { + const phaserLayer = this.findPhaserLayer(layer); + if (phaserLayer) { + if (tile === null) { + phaserLayer.putTileAt(-1, x, y); + return; + } + const tileIndex = this.getIndexForTileType(tile); + if (tileIndex !== undefined) { + this.putTileInFlatLayer(tileIndex, x, y, layer); + const phaserTile = phaserLayer.putTileAt(tileIndex, x, y); + for (const property of this.getTileProperty(tileIndex)) { + if (property.name === GameMapProperties.COLLIDES && property.value) { + phaserTile.setCollision(true); + } + } + } else { + console.error("The tile '" + tile + "' that you want to place doesn't exist."); + } + } else { + console.error("The layer '" + layer + "' does not exist (or is not a tilelaye)."); + } + } + + public setLayerProperty( + layerName: string, + propertyName: string, + propertyValue: string | number | undefined | boolean + ) { + const layer = this.findLayer(layerName); + if (layer === undefined) { + console.warn('Could not find layer "' + layerName + '" when calling setProperty'); + return; + } + if (layer.properties === undefined) { + layer.properties = []; + } + const property = layer.properties.find((property) => property.name === propertyName); + if (property === undefined) { + if (propertyValue === undefined) { + return; + } + layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); + return; + } + if (propertyValue === undefined) { + const index = layer.properties.indexOf(property); + layer.properties.splice(index, 1); + } + property.value = propertyValue; + + this.triggerAllProperties(); + this.triggerLayersChange(); + } + + /** + * Trigger all the callbacks (used when exiting a map) + */ + public triggerExitCallbacks(): void { + const emptyProps = new Map(); + for (const [oldPropName, oldPropValue] of this.lastProperties.entries()) { + // We found a property that disappeared + this.trigger(oldPropName, oldPropValue, undefined, emptyProps); + } + } + + private getLayersByKey(key: number): Array { + return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0); + } + + private isCollidingAt(x: number, y: number): boolean { + for (const layer of this.phaserLayers) { + if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) { + return true; + } + } + return false; + } + private triggerAllProperties(): void { const newProps = this.getProperties(this.key ?? 0); const oldProps = this.lastProperties; @@ -247,10 +402,6 @@ export class GameMap { } } - public getCurrentProperties(): Map { - return this.lastProperties; - } - private getProperties(key: number): Map { const properties = new Map(); @@ -292,10 +443,6 @@ export class GameMap { return properties; } - public getMap(): ITiledMap { - return this.map; - } - private getTileProperty(index: number): Array { if (this.tileSetPropertyMap[index]) { return this.tileSetPropertyMap[index]; @@ -317,64 +464,6 @@ export class GameMap { } } - /** - * Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on. - */ - public onPropertyChange(propName: string, callback: PropertyChangeCallback) { - let callbacksArray = this.propertiesChangeCallbacks.get(propName); - if (callbacksArray === undefined) { - callbacksArray = new Array(); - this.propertiesChangeCallbacks.set(propName, callbacksArray); - } - callbacksArray.push(callback); - } - - /** - * Registers a callback called when the user moves inside another layer. - */ - public onEnterLayer(callback: layerChangeCallback) { - this.enterLayerCallbacks.push(callback); - } - - /** - * Registers a callback called when the user moves outside another layer. - */ - public onLeaveLayer(callback: layerChangeCallback) { - this.leaveLayerCallbacks.push(callback); - } - - /** - * Registers a callback called when the user moves inside another zone. - */ - public onEnterZone(callback: zoneChangeCallback) { - this.enterZoneCallbacks.push(callback); - } - - /** - * Registers a callback called when the user moves outside another zone. - */ - public onLeaveZone(callback: zoneChangeCallback) { - this.leaveZoneCallbacks.push(callback); - } - - public findLayer(layerName: string): ITiledMapLayer | undefined { - return this.flatLayers.find((layer) => layer.name === layerName); - } - - public findPhaserLayer(layerName: string): TilemapLayer | undefined { - return this.phaserLayers.find((layer) => layer.layer.name === layerName); - } - - public findPhaserLayers(groupName: string): TilemapLayer[] { - return this.phaserLayers.filter((l) => l.layer.name.includes(groupName)); - } - - public addTerrain(terrain: Phaser.Tilemaps.Tileset): void { - for (const phaserLayer of this.phaserLayers) { - phaserLayer.tileset.push(terrain); - } - } - private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void { const fLayer = this.findLayer(layer); if (fLayer == undefined) { @@ -396,30 +485,6 @@ export class GameMap { fLayer.data[x + y * fLayer.width] = index; } - public putTile(tile: string | number | null, x: number, y: number, layer: string): void { - const phaserLayer = this.findPhaserLayer(layer); - if (phaserLayer) { - if (tile === null) { - phaserLayer.putTileAt(-1, x, y); - return; - } - const tileIndex = this.getIndexForTileType(tile); - if (tileIndex !== undefined) { - this.putTileInFlatLayer(tileIndex, x, y, layer); - const phaserTile = phaserLayer.putTileAt(tileIndex, x, y); - for (const property of this.getTileProperty(tileIndex)) { - if (property.name === GameMapProperties.COLLIDES && property.value) { - phaserTile.setCollision(true); - } - } - } else { - console.error("The tile '" + tile + "' that you want to place doesn't exist."); - } - } else { - console.error("The layer '" + layer + "' does not exist (or is not a tilelaye)."); - } - } - private getIndexForTileType(tile: string | number): number | undefined { if (typeof tile == "number") { return tile; @@ -427,48 +492,6 @@ export class GameMap { return this.tileNameMap.get(tile); } - public setLayerProperty( - layerName: string, - propertyName: string, - propertyValue: string | number | undefined | boolean - ) { - const layer = this.findLayer(layerName); - if (layer === undefined) { - console.warn('Could not find layer "' + layerName + '" when calling setProperty'); - return; - } - if (layer.properties === undefined) { - layer.properties = []; - } - const property = layer.properties.find((property) => property.name === propertyName); - if (property === undefined) { - if (propertyValue === undefined) { - return; - } - layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); - return; - } - if (propertyValue === undefined) { - const index = layer.properties.indexOf(property); - layer.properties.splice(index, 1); - } - property.value = propertyValue; - - this.triggerAllProperties(); - this.triggerLayersChange(); - } - - /** - * Trigger all the callbacks (used when exiting a map) - */ - public triggerExitCallbacks(): void { - const emptyProps = new Map(); - for (const [oldPropName, oldPropValue] of this.lastProperties.entries()) { - // We found a property that disappeared - this.trigger(oldPropName, oldPropValue, undefined, emptyProps); - } - } - private getObjectsFromLayers(layers: ITiledMapLayer[]): ITiledMapObject[] { const objects: ITiledMapObject[] = []; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7b8314f0..26e4004a 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -48,9 +48,9 @@ import { PropertyUtils } from "../Map/PropertyUtils"; import { GameMapPropertiesListener } from "./GameMapPropertiesListener"; import { analyticsClient } from "../../Administration/AnalyticsClient"; import { GameMapProperties } from "./GameMapProperties"; +import { PathfindingManager } from "../../Utils/PathfindingManager"; import type { GroupCreatedUpdatedMessageInterface, - MessageUserJoined, MessageUserMovedInterface, MessageUserPositionInterface, OnConnectInterface, @@ -66,7 +66,6 @@ import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITi import type { AddPlayerInterface } from "./AddPlayerInterface"; import { CameraManager, CameraManagerEvent, CameraManagerEventCameraUpdateData } from "./CameraManager"; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; -import type { Character } from "../Entity/Character"; import { peerStore } from "../../Stores/PeerStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; @@ -89,9 +88,9 @@ import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile; import { deepCopy } from "deep-copy-ts"; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import { MapStore } from "../../Stores/Utils/MapStore"; -import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore"; -import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator"; +import { followUsersColorStore } from "../../Stores/FollowStore"; import Camera = Phaser.Cameras.Scene2D.Camera; +import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -203,6 +202,7 @@ export class GameScene extends DirtyScene { private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time. private emoteManager!: EmoteManager; private cameraManager!: CameraManager; + private pathfindingManager!: PathfindingManager; private preloading: boolean = true; private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; @@ -549,7 +549,7 @@ export class GameScene extends DirtyScene { this.MapPlayers = this.physics.add.group({ immovable: true }); //create input to move - this.userInputManager = new UserInputManager(this); + this.userInputManager = new UserInputManager(this, new GameSceneUserInputHandler(this)); mediaManager.setUserInputManager(this.userInputManager); if (localUserStore.getFullscreen()) { @@ -568,6 +568,8 @@ export class GameScene extends DirtyScene { { x: this.Map.widthInPixels, y: this.Map.heightInPixels }, waScaleManager ); + + this.pathfindingManager = new PathfindingManager(this, this.gameMap.getCollisionsGrid()); biggestAvailableAreaStore.recompute(); this.cameraManager.startFollowPlayer(this.CurrentPlayer); @@ -605,10 +607,6 @@ export class GameScene extends DirtyScene { scriptPromises.push(iframeListener.registerScript(script, !disableModuleMode)); } - this.userInputManager.spaceEvent(() => { - this.outlinedItem?.activate(); - }); - this.reposition(); // From now, this game scene will be notified of reposition events @@ -677,6 +675,10 @@ export class GameScene extends DirtyScene { ); } + public activateOutlinedItem(): void { + this.outlinedItem?.activate(); + } + /** * Initializes the connection to Pusher. */ @@ -1688,7 +1690,6 @@ ${escapedMessage} texturesPromise, PlayerAnimationDirections.Down, false, - this.userInputManager, this.companion, this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined ); @@ -1808,7 +1809,7 @@ ${escapedMessage} update(time: number, delta: number): void { this.dirty = false; this.currentTick = time; - this.CurrentPlayer.moveUser(delta); + this.CurrentPlayer.moveUser(delta, this.userInputManager.getEventListForGameTick()); // Let's handle all events while (this.pendingEvents.length !== 0) { @@ -2172,4 +2173,16 @@ ${escapedMessage} this.scene.stop(this.scene.key); this.scene.remove(this.scene.key); } + + public getGameMap(): GameMap { + return this.gameMap; + } + + public getCameraManager(): CameraManager { + return this.cameraManager; + } + + public getPathfindingManager(): PathfindingManager { + return this.pathfindingManager; + } } diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index e41b3237..451a057b 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -1,8 +1,7 @@ import { PlayerAnimationDirections } from "./Animation"; import type { GameScene } from "../Game/GameScene"; -import { ActiveEventList, UserInputEvent, UserInputManager } from "../UserInput/UserInputManager"; +import { ActiveEventList, UserInputEvent } from "../UserInput/UserInputManager"; import { Character } from "../Entity/Character"; -import type { RemotePlayer } from "../Entity/RemotePlayer"; import { get } from "svelte/store"; import { userMovingStore } from "../../Stores/GameStore"; @@ -12,6 +11,8 @@ export const hasMovedEventName = "hasMoved"; export const requestEmoteEventName = "requestEmote"; export class Player extends Character { + private pathToFollow?: { x: number; y: number }[]; + constructor( Scene: GameScene, x: number, @@ -20,7 +21,6 @@ export class Player extends Character { texturesPromise: Promise, direction: PlayerAnimationDirections, moving: boolean, - private userInputManager: UserInputManager, companion: string | null, companionTexturePromise?: Promise ) { @@ -30,6 +30,55 @@ export class Player extends Character { this.getBody().setImmovable(false); } + public moveUser(delta: number, activeUserInputEvents: ActiveEventList): void { + const state = get(followStateStore); + const role = get(followRoleStore); + + if (activeUserInputEvents.get(UserInputEvent.Follow)) { + if (state === "off" && this.scene.groups.size > 0) { + this.sendFollowRequest(); + } else if (state === "active") { + followStateStore.set("ending"); + } + } + + if (this.pathToFollow && activeUserInputEvents.anyExcept(UserInputEvent.SpeedUp)) { + this.pathToFollow = undefined; + } + + let x = 0; + let y = 0; + if ((state === "active" || state === "ending") && role === "follower") { + [x, y] = this.computeFollowMovement(); + } + if (this.pathToFollow) { + [x, y] = this.computeFollowPathMovement(); + } + this.inputStep(activeUserInputEvents, x, y); + } + + public sendFollowRequest() { + this.scene.connection?.emitFollowRequest(); + followRoleStore.set("leader"); + followStateStore.set("active"); + } + + public startFollowing() { + followStateStore.set("active"); + this.scene.connection?.emitFollowConfirmation(); + } + + public setPathToFollow(path: { x: number; y: number }[]): void { + // take collider offset into consideraton + this.pathToFollow = this.adjustPathToFollowToColliderBounds(path); + } + + private adjustPathToFollowToColliderBounds(path: { x: number; y: number }[]): { x: number; y: number }[] { + return path.map((step) => { + return { x: step.x, y: step.y - this.getBody().offset.y }; + }); + } + private inputStep(activeEvents: ActiveEventList, x: number, y: number) { // Process input events if (activeEvents.get(UserInputEvent.MoveUp)) { @@ -95,40 +144,29 @@ export class Player extends Character { if (distance < 2000) { return [0, 0]; } - const xMovement = xDistance / Math.sqrt(distance); - const yMovement = yDistance / Math.sqrt(distance); - return [xMovement, yMovement]; + return this.getMovementDirection(xDistance, yDistance, distance); } - public moveUser(delta: number): void { - const activeEvents = this.userInputManager.getEventListForGameTick(); - const state = get(followStateStore); - const role = get(followRoleStore); - - if (activeEvents.get(UserInputEvent.Follow)) { - if (state === "off" && this.scene.groups.size > 0) { - this.sendFollowRequest(); - } else if (state === "active") { - followStateStore.set("ending"); - } + private computeFollowPathMovement(): number[] { + if (this.pathToFollow?.length === 0) { + this.pathToFollow = undefined; } - - let x = 0; - let y = 0; - if ((state === "active" || state === "ending") && role === "follower") { - [x, y] = this.computeFollowMovement(); + if (!this.pathToFollow) { + return [0, 0]; } - this.inputStep(activeEvents, x, y); + const nextStep = this.pathToFollow[0]; + + // Compute movement direction + const xDistance = nextStep.x - this.x; + const yDistance = nextStep.y - this.y; + const distance = Math.pow(xDistance, 2) + Math.pow(yDistance, 2); + if (distance < 200) { + this.pathToFollow.shift(); + } + return this.getMovementDirection(xDistance, yDistance, distance); } - public sendFollowRequest() { - this.scene.connection?.emitFollowRequest(); - followRoleStore.set("leader"); - followStateStore.set("active"); - } - - public startFollowing() { - followStateStore.set("active"); - this.scene.connection?.emitFollowConfirmation(); + private getMovementDirection(xDistance: number, yDistance: number, distance: number): [number, number] { + return [xDistance / Math.sqrt(distance), yDistance / Math.sqrt(distance)]; } } diff --git a/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts b/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts new file mode 100644 index 00000000..2915901d --- /dev/null +++ b/front/src/Phaser/UserInput/GameSceneUserInputHandler.ts @@ -0,0 +1,58 @@ +import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface"; +import type { GameScene } from "../Game/GameScene"; + +export class GameSceneUserInputHandler implements UserInputHandlerInterface { + private gameScene: GameScene; + + constructor(gameScene: GameScene) { + this.gameScene = gameScene; + } + + public handleMouseWheelEvent( + pointer: Phaser.Input.Pointer, + gameObjects: Phaser.GameObjects.GameObject[], + deltaX: number, + deltaY: number, + deltaZ: number + ): void { + this.gameScene.zoomByFactor(1 - (deltaY / 53) * 0.1); + } + + public handlePointerUpEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void { + if (pointer.rightButtonReleased() || pointer.getDuration() > 250) { + return; + } + const camera = this.gameScene.getCameraManager().getCamera(); + const index = this.gameScene + .getGameMap() + .getTileIndexAt(pointer.x + camera.scrollX, pointer.y + camera.scrollY); + const startTile = this.gameScene + .getGameMap() + .getTileIndexAt(this.gameScene.CurrentPlayer.x, this.gameScene.CurrentPlayer.y); + this.gameScene + .getPathfindingManager() + .findPath(startTile, index, true) + .then((path) => { + const tileDimensions = this.gameScene.getGameMap().getTileDimensions(); + const pixelPath = path.map((step) => { + return { + x: step.x * tileDimensions.width + tileDimensions.width * 0.5, + y: step.y * tileDimensions.height + tileDimensions.height * 0.5, + }; + }); + // Remove first step as it is for the tile we are currently standing on + pixelPath.shift(); + this.gameScene.CurrentPlayer.setPathToFollow(pixelPath); + }) + .catch((reason) => { + console.warn(reason); + }); + } + + public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {} + + public handleSpaceKeyUpEvent(event: Event): Event { + this.gameScene.activateOutlinedItem(); + return event; + } +} diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index e00e1e86..ffaf7d1f 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -1,8 +1,8 @@ -import type { GameScene } from "../Game/GameScene"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { MobileJoystick } from "../Components/MobileJoystick"; import { enableUserInputsStore } from "../../Stores/UserInputStore"; import type { Direction } from "phaser3-rex-plugins/plugins/virtualjoystick.js"; +import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface"; interface UserInputManagerDatum { keyInstance: Phaser.Input.Keyboard.Key; @@ -37,12 +37,21 @@ export class ActiveEventList { any(): boolean { return Array.from(this.eventMap.values()).reduce((accu, curr) => accu || curr, false); } + anyExcept(...exceptions: UserInputEvent[]): boolean { + const userInputEvents = Array.from(this.eventMap); + for (const event of userInputEvents) { + if (event[1] && !exceptions.includes(event[0])) { + return true; + } + } + return false; + } } //this class is responsible for catching user inputs and listing all active user actions at every game tick events. export class UserInputManager { - private KeysCode!: UserInputManagerDatum[]; - private Scene: GameScene; + private keysCode!: UserInputManagerDatum[]; + private scene: Phaser.Scene; private isInputDisabled: boolean; private joystick!: MobileJoystick; @@ -51,11 +60,15 @@ export class UserInputManager { private joystickForceAccuX = 0; private joystickForceAccuY = 0; - constructor(Scene: GameScene) { - this.Scene = Scene; + private userInputHandler: UserInputHandlerInterface; + + constructor(scene: Phaser.Scene, userInputHandler: UserInputHandlerInterface) { + this.scene = scene; + this.userInputHandler = userInputHandler; + this.isInputDisabled = false; this.initKeyBoardEvent(); - this.initMouseWheel(); + this.bindInputEventHandlers(); if (touchScreenManager.supportTouchScreen) { this.initVirtualJoystick(); } @@ -66,7 +79,7 @@ export class UserInputManager { } initVirtualJoystick() { - this.joystick = new MobileJoystick(this.Scene); + this.joystick = new MobileJoystick(this.scene); this.joystick.on("update", () => { this.joystickForceAccuX = this.joystick.forceX ? this.joystickForceAccuX : 0; this.joystickForceAccuY = this.joystick.forceY ? this.joystickForceAccuY : 0; @@ -92,80 +105,80 @@ export class UserInputManager { } initKeyBoardEvent() { - this.KeysCode = [ + this.keysCode = [ { event: UserInputEvent.MoveUp, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false), }, { event: UserInputEvent.MoveUp, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false), }, { event: UserInputEvent.MoveLeft, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false), }, { event: UserInputEvent.MoveLeft, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false), }, { event: UserInputEvent.MoveDown, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false), }, { event: UserInputEvent.MoveRight, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false), }, { event: UserInputEvent.MoveUp, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false), }, { event: UserInputEvent.MoveLeft, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false), }, { event: UserInputEvent.MoveDown, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false), }, { event: UserInputEvent.MoveRight, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false), }, { event: UserInputEvent.SpeedUp, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false), }, { event: UserInputEvent.Interact, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false), }, { event: UserInputEvent.Interact, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false), }, { event: UserInputEvent.Follow, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false), }, { event: UserInputEvent.Shout, - keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false), + keyInstance: this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false), }, ]; } clearAllListeners() { - this.Scene.input.keyboard.removeAllListeners(); + this.scene.input.keyboard.removeAllListeners(); } //todo: should we also disable the joystick? disableControls() { - this.Scene.input.keyboard.removeAllKeys(); + this.scene.input.keyboard.removeAllKeys(); this.isInputDisabled = true; } @@ -201,7 +214,7 @@ export class UserInputManager { } }); eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any()); - this.KeysCode.forEach((d) => { + this.keysCode.forEach((d) => { if (d.keyInstance.isDown) { eventsMap.set(d.event, true); } @@ -209,30 +222,60 @@ export class UserInputManager { return eventsMap; } - spaceEvent(callback: Function) { - this.Scene.input.keyboard.on("keyup-SPACE", (event: Event) => { - callback(); - return event; - }); - } - addSpaceEventListner(callback: Function) { - this.Scene.input.keyboard.addListener("keyup-SPACE", callback); + this.scene.input.keyboard.addListener("keyup-SPACE", callback); } removeSpaceEventListner(callback: Function) { - this.Scene.input.keyboard.removeListener("keyup-SPACE", callback); + this.scene.input.keyboard.removeListener("keyup-SPACE", callback); } destroy(): void { this.joystick?.destroy(); } - private initMouseWheel() { - this.Scene.input.on( - "wheel", - (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => { - this.Scene.zoomByFactor(1 - (deltaY / 53) * 0.1); + private bindInputEventHandlers() { + this.scene.input.on( + Phaser.Input.Events.POINTER_WHEEL, + ( + pointer: Phaser.Input.Pointer, + gameObjects: Phaser.GameObjects.GameObject[], + deltaX: number, + deltaY: number, + deltaZ: number + ) => { + if (this.isInputDisabled) { + return; + } + this.userInputHandler.handleMouseWheelEvent(pointer, gameObjects, deltaX, deltaY, deltaZ); } ); + + this.scene.input.on( + Phaser.Input.Events.POINTER_UP, + (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => { + this.joystick.hide(); + this.userInputHandler.handlePointerUpEvent(pointer, gameObjects); + } + ); + + this.scene.input.on( + Phaser.Input.Events.POINTER_DOWN, + (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => { + if (!pointer.wasTouch) { + return; + } + this.userInputHandler.handlePointerDownEvent(pointer, gameObjects); + // Let's only display the joystick if there is one finger on the screen + if ((pointer.event as TouchEvent).touches.length === 1) { + this.joystick.showAt(pointer.x, pointer.y); + } else { + this.joystick.hide(); + } + } + ); + + this.scene.input.keyboard.on("keyup-SPACE", (event: Event) => { + this.userInputHandler.handleSpaceKeyUpEvent(event); + }); } } diff --git a/front/src/Utils/MathUtils.ts b/front/src/Utils/MathUtils.ts index aea3bb11..c2fc88a2 100644 --- a/front/src/Utils/MathUtils.ts +++ b/front/src/Utils/MathUtils.ts @@ -22,4 +22,13 @@ export class MathUtils { public static isBetween(value: number, min: number, max: number): boolean { return value >= min && value <= max; } + + public static distanceBetween( + p1: { x: number; y: number }, + p2: { x: number; y: number }, + squared: boolean = true + ): number { + const distance = Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); + return squared ? Math.sqrt(distance) : distance; + } } diff --git a/front/src/Utils/PathfindingManager.ts b/front/src/Utils/PathfindingManager.ts new file mode 100644 index 00000000..2d0cae27 --- /dev/null +++ b/front/src/Utils/PathfindingManager.ts @@ -0,0 +1,109 @@ +import * as EasyStar from "easystarjs"; +import { MathUtils } from "./MathUtils"; + +export class PathfindingManager { + private scene: Phaser.Scene; + + private easyStar; + private grid: number[][]; + + constructor(scene: Phaser.Scene, collisionsGrid: number[][]) { + this.scene = scene; + + this.easyStar = new EasyStar.js(); + this.easyStar.enableDiagonals(); + + this.grid = collisionsGrid; + this.setEasyStarGrid(collisionsGrid); + } + + public async findPath( + start: { x: number; y: number }, + end: { x: number; y: number }, + tryFindingNearestAvailable: boolean = false + ): Promise<{ x: number; y: number }[]> { + let endPoints: { x: number; y: number }[] = [end]; + if (tryFindingNearestAvailable) { + endPoints = [ + end, + ...this.getNeighbouringTiles(end).sort((a, b) => { + const aDist = MathUtils.distanceBetween(a, start, false); + const bDist = MathUtils.distanceBetween(b, start, false); + if (aDist > bDist) { + return 1; + } + if (aDist < bDist) { + return -1; + } + return 0; + }), + ]; + } + let path: { x: number; y: number }[] = []; + while (endPoints.length > 0) { + const endPoint = endPoints.shift(); + if (!endPoint) { + return []; + } + // rejected Promise will return undefined for path + path = await this.getPath(start, endPoint).catch(); + if (path && path.length > 0) { + return path; + } + } + return []; + } + + private getNeighbouringTiles(tile: { x: number; y: number }): { x: number; y: number }[] { + const xOffsets = [-1, 0, 1, 1, 1, 0, -1, -1]; + const yOffsets = [-1, -1, -1, 0, 1, 1, 1, 0]; + + const neighbours: { x: number; y: number }[] = []; + for (let i = 0; i < 8; i += 1) { + const tileToCheck = { x: tile.x + xOffsets[i], y: tile.y + yOffsets[i] }; + if (this.isTileWithinMap(tileToCheck)) { + neighbours.push(tileToCheck); + } + } + return neighbours; + } + + private isTileWithinMap(tile: { x: number; y: number }): boolean { + const mapHeight = this.grid.length ?? 0; + const mapWidth = this.grid[0]?.length ?? 0; + + return MathUtils.isBetween(tile.x, 0, mapWidth) && MathUtils.isBetween(tile.y, 0, mapHeight); + } + + /** + * Returns empty array if path was not found + */ + private async getPath( + start: { x: number; y: number }, + end: { x: number; y: number } + ): Promise<{ x: number; y: number }[]> { + return new Promise((resolve, reject) => { + this.easyStar.findPath(start.x, start.y, end.x, end.y, (path) => { + if (path === null) { + resolve([]); + } else { + resolve(path); + } + }); + this.easyStar.calculate(); + }); + } + + private setEasyStarGrid(grid: number[][]): void { + this.easyStar.setGrid(grid); + this.easyStar.setAcceptableTiles([0]); // zeroes are walkable + } + + private logGridToTheConsole(grid: number[][]): void { + let rowNumber = 0; + for (const row of grid) { + console.log(`${rowNumber}:\t${row}`); + rowNumber += 1; + } + } +} diff --git a/front/src/index.ts b/front/src/index.ts index a2064cd8..d3017ad2 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -102,6 +102,7 @@ const config: GameConfig = { dom: { createContainer: true, }, + disableContextMenu: true, render: { pixelArt: true, roundPixels: true, diff --git a/front/yarn.lock b/front/yarn.lock index 857b571e..d2ac31b3 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1990,6 +1990,13 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +easystarjs@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/easystarjs/-/easystarjs-0.4.4.tgz#8cec6d20d0d8660715da0301d1da440370a8f40a" + integrity sha512-ZSt0TkB8xuIXRIrKsM3jkmk1/cZUtyvf0DqOXf6wuKq9slx9UA5kkLtiaWhtmOQFJFKdabbvXwk6RO0znghArQ== + dependencies: + heap "0.2.6" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2925,6 +2932,11 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +heap@0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" + integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"