diff --git a/front/src/Phaser/Game/CameraManager.ts b/front/src/Phaser/Game/CameraManager.ts new file mode 100644 index 00000000..595867be --- /dev/null +++ b/front/src/Phaser/Game/CameraManager.ts @@ -0,0 +1,70 @@ +import { Easing } from '../../types'; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import type { Box } from '../../WebRtc/LayoutManager'; +import type { WaScaleManager } from '../Services/WaScaleManager'; +import type { GameScene } from './GameScene'; + +export class CameraManager extends Phaser.Events.EventEmitter { + + private scene: GameScene; + private camera: Phaser.Cameras.Scene2D.Camera; + private waScaleManager: WaScaleManager; + + private cameraBounds: { x: number, y: number }; + + constructor(scene: GameScene, cameraBounds: { x: number, y: number }, waScaleManager: WaScaleManager) { + super(); + this.scene = scene; + + this.camera = scene.cameras.main; + this.cameraBounds = cameraBounds; + + this.waScaleManager = waScaleManager; + + this.initCamera(); + } + + public getCamera(): Phaser.Cameras.Scene2D.Camera { + return this.camera; + } + + public changeCameraFocus(focusOn: { x: number, y: number, width: number, height: number }, duration: number = 1000): void { + const maxZoomModifier = 2.84; // How to get max zoom value? + const currentZoomModifier = this.waScaleManager.zoomModifier; + const zoomModifierChange = maxZoomModifier - currentZoomModifier; + this.camera.stopFollow(); + this.camera.pan( + focusOn.x + focusOn.width * 0.5, + focusOn.y + focusOn.height * 0.5, + duration, + Easing.SineEaseOut, false, (camera, progress, x, y) => { + this.scene.setZoomModifierTo(currentZoomModifier + progress * zoomModifierChange); + }); + } + + public startFollow(target: object | Phaser.GameObjects.GameObject): void { + this.camera.startFollow(target, true); + } + + /** + * Updates the offset of the character compared to the center of the screen according to the layout manager + * (tries to put the character in the center of the remaining space if there is a discussion going on. + */ + public updateCameraOffset(array: Box): void { + const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; + const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; + + const game = HtmlUtils.querySelectorOrFail("#game canvas"); + // Let's put this in Game coordinates by applying the zoom level: + + this.camera.setFollowOffset( + ((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scene.scale.zoom, + ((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scene.scale.zoom + ); + } + + private initCamera() { + this.camera = this.scene.cameras.main; + this.camera.setBounds(0, 0, this.cameraBounds.x, this.cameraBounds.y); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index db9b652e..85a1a0e0 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -192,6 +192,7 @@ export class GameMap { const zones = this.tiledObjects.filter(object => object.type === "zone"); // P.H. NOTE: We could also get all of the zones and add properties of occupied tiles to them, so we could later on check collision by using tileKeys + // TODO: Change this to an array with currently occupied sone instead of doing elimination process const zonesByOldPosition = this.oldPosition ? zones.filter((zone) => { if (!this.oldPosition) { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8bf0f13e..d596fa56 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -60,6 +60,7 @@ import { PinchManager } from "../UserInput/PinchManager"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; import { waScaleManager } from "../Services/WaScaleManager"; import { EmoteManager } from "./EmoteManager"; +import { CameraManager } from './CameraManager'; import EVENT_TYPE = Phaser.Scenes.Events; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; @@ -198,6 +199,7 @@ export class GameScene extends DirtyScene { private pinchManager: PinchManager | undefined; private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time. private emoteManager!: EmoteManager; + private cameraManager!: CameraManager; private preloading: boolean = true; private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; @@ -549,7 +551,9 @@ export class GameScene extends DirtyScene { this.createCurrentPlayer(); this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted - this.initCamera(); + this.cameraManager = new CameraManager(this, { x: this.Map.widthInPixels, y: this.Map.heightInPixels }, waScaleManager); + biggestAvailableAreaStore.recompute(); + this.cameraManager.startFollow(this.CurrentPlayer); this.animatedTiles.init(this.Map); this.events.on("tileanimationupdate", () => (this.dirty = true)); @@ -590,7 +594,7 @@ export class GameScene extends DirtyScene { // From now, this game scene will be notified of reposition events this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => - this.updateCameraOffset(box) + this.cameraManager.updateCameraOffset(box) ); new GameMapPropertiesListener(this, this.gameMap).register(); @@ -643,7 +647,7 @@ export class GameScene extends DirtyScene { * Initializes the connection to Pusher. */ private connect(): void { - const camera = this.cameras.main; + const camera = this.cameraManager.getCamera(); connectionManager .connectToRoomSocket( @@ -779,17 +783,30 @@ export class GameScene extends DirtyScene { }); }); + // P.H. TODO: Send those events to the iframe? this.gameMap.onEnterZone((zones) => { - console.log('enter zones'); - console.log(zones); + for (const zone of zones) { + for (const property of zone.properties ?? []) { + if (property.name === 'focusable' && property.value === true) { + this.cameraManager.changeCameraFocus(zone); + break; + } + } + } // zones.forEach((zone) => { // iframeListener.sendEnterLayerEvent(zone.name); // }); }); this.gameMap.onLeaveZone((zones) => { - console.log('leave zones'); - console.log(zones); + for (const zone of zones) { + for (const property of zone.properties ?? []) { + if (property.name === 'focusable' && property.value === true) { + this.cameraManager.startFollow(this.CurrentPlayer); + break; + } + } + } // zones.forEach((zone) => { // iframeListener.sendEnterLayerEvent(zone.name); // }); @@ -1478,13 +1495,6 @@ ${escapedMessage} } } - //todo: in a dedicated class/function? - initCamera() { - this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); - this.cameras.main.startFollow(this.CurrentPlayer, true); - biggestAvailableAreaStore.recompute(); - } - createCollisionWithPlayer() { //add collision layer for (const phaserLayer of this.gameMap.phaserLayers) { @@ -1876,23 +1886,6 @@ ${escapedMessage} biggestAvailableAreaStore.recompute(); } - /** - * Updates the offset of the character compared to the center of the screen according to the layout manager - * (tries to put the character in the center of the remaining space if there is a discussion going on. - */ - private updateCameraOffset(array: Box): void { - const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; - const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; - - const game = HtmlUtils.querySelectorOrFail("#game canvas"); - // Let's put this in Game coordinates by applying the zoom level: - - this.cameras.main.setFollowOffset( - ((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scale.zoom, - ((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scale.zoom - ); - } - public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring( @@ -1965,6 +1958,10 @@ ${escapedMessage} biggestAvailableAreaStore.recompute(); } + public setZoomModifierTo(value: number): void { + waScaleManager.zoomModifier = value; + } + public createSuccessorGameScene(autostart: boolean, reconnecting: boolean) { const gameSceneKey = "somekey" + Math.round(Math.random() * 10000); const game = new GameScene(this.room, this.MapUrlFile, gameSceneKey); diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts index 5ceaeb71..3fb1d29b 100644 --- a/front/src/Phaser/Services/WaScaleManager.ts +++ b/front/src/Phaser/Services/WaScaleManager.ts @@ -5,7 +5,7 @@ import type { Game } from "../Game/Game"; import { ResizableScene } from "../Login/ResizableScene"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; -class WaScaleManager { +export class WaScaleManager { private hdpiManager: HdpiManager; private scaleManager!: ScaleManager; private game!: Game; diff --git a/front/src/types.ts b/front/src/types.ts index d957a2c2..409070e5 100644 --- a/front/src/types.ts +++ b/front/src/types.ts @@ -21,3 +21,34 @@ export interface IVirtualJoystick extends Phaser.GameObjects.GameObject { visible: boolean; createCursorKeys: () => CursorKeys; } + +export enum Easing { + Linear = "Linear", + QuadEaseIn = "Quad.easeIn", + CubicEaseIn = "Cubic.easeIn", + QuartEaseIn = "Quart.easeIn", + QuintEaseIn = "Quint.easeIn", + SineEaseIn = "Sine.easeIn", + ExpoEaseIn = "Expo.easeIn", + CircEaseIn = "Circ.easeIn", + BackEaseIn = "Back.easeIn", + BounceEaseIn = "Bounce.easeIn", + QuadEaseOut = "Quad.easeOut", + CubicEaseOut = "Cubic.easeOut", + QuartEaseOut = "Quart.easeOut", + QuintEaseOut = "Quint.easeOut", + SineEaseOut = "Sine.easeOut", + ExpoEaseOut = "Expo.easeOut", + CircEaseOut = "Circ.easeOut", + BackEaseOut = "Back.easeOut", + BounceEaseOut = "Bounce.easeOut", + QuadEaseInOut = "Quad.easeInOut", + CubicEaseInOut = "Cubic.easeInOut", + QuartEaseInOut = "Quart.easeInOut", + QuintEaseInOut = "Quint.easeInOut", + SineEaseInOut = "Sine.easeInOut", + ExpoEaseInOut = "Expo.easeInOut", + CircEaseInOut = "Circ.easeInOut", + BackEaseInOut = "Back.easeInOut", + BounceEaseInOut = "Bounce.easeInOut" +} diff --git a/maps/starter/map.json b/maps/starter/map.json index 31d81b19..2ef0516d 100644 --- a/maps/starter/map.json +++ b/maps/starter/map.json @@ -86,7 +86,7 @@ "y":0 }, { - "data":[201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201], + "data":[201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 223, 223, 223, 223, 223, 223, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 212, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 212, 212, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201], "height":17, "id":4, "name":"floor", @@ -170,6 +170,28 @@ "width":192, "x":32, "y":96 + }, + { + "height":64, + "id":11, + "name":"coffeeZone", + "properties":[ + { + "name":"display_name", + "type":"string", + "value":"Coffee Time!" + }, + { + "name":"focusable", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"zone", + "visible":true, + "width":64, + "x":64, + "y":288 }], "opacity":1, "type":"objectgroup", @@ -214,7 +236,7 @@ "y":0 }], "nextlayerid":39, - "nextobjectid":11, + "nextobjectid":12, "orientation":"orthogonal", "properties":[ {