workadventure/front/src/Phaser/Game/CameraManager.ts
2022-01-14 11:41:37 +01:00

284 lines
11 KiB
TypeScript

import { Easing } from "../../types";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type { Box } from "../../WebRtc/LayoutManager";
import { hasMovedEventName, Player } from "../Player/Player";
import { WaScaleManager, WaScaleManagerEvent, WaScaleManagerFocusTarget } from "../Services/WaScaleManager";
import type { GameScene } from "./GameScene";
export enum CameraMode {
/**
* Camera looks at certain point but is not locked and will start following the player on his movement
*/
Positioned = "Positioned",
/**
* Camera is actively following the player
*/
Follow = "Follow",
/**
* Camera is focusing on certain point and will not break this focus even on player movement
*/
Focus = "Focus",
}
export enum CameraManagerEvent {
CameraUpdate = "CameraUpdate",
}
export interface CameraManagerEventCameraUpdateData {
x: number;
y: number;
width: number;
height: number;
zoom: number;
}
export class CameraManager extends Phaser.Events.EventEmitter {
private scene: GameScene;
private camera: Phaser.Cameras.Scene2D.Camera;
private cameraBounds: { x: number; y: number };
private waScaleManager: WaScaleManager;
private cameraMode: CameraMode = CameraMode.Positioned;
private restoreZoomTween?: Phaser.Tweens.Tween;
private startFollowTween?: Phaser.Tweens.Tween;
private playerToFollow?: Player;
private cameraLocked: boolean;
constructor(scene: GameScene, cameraBounds: { x: number; y: number }, waScaleManager: WaScaleManager) {
super();
this.scene = scene;
this.camera = scene.cameras.main;
this.cameraBounds = cameraBounds;
this.cameraLocked = false;
this.waScaleManager = waScaleManager;
this.initCamera();
this.bindEventHandlers();
}
public destroy(): void {
this.scene.game.events.off(WaScaleManagerEvent.RefreshFocusOnTarget);
super.destroy();
}
public getCamera(): Phaser.Cameras.Scene2D.Camera {
return this.camera;
}
/**
* Set camera view to specific destination without changing current camera mode. Won't work if camera mode is set to Focus.
* @param setTo Viewport on which the camera should set the position
* @param duration Time for the transition im MS. If set to 0, transition will occur immediately
*/
public setPosition(setTo: WaScaleManagerFocusTarget, duration: number = 1000): void {
if (this.cameraMode === CameraMode.Focus) {
return;
}
this.setCameraMode(CameraMode.Positioned);
this.waScaleManager.saveZoom();
this.camera.stopFollow();
const currentZoomModifier = this.waScaleManager.zoomModifier;
const zoomModifierChange = this.getZoomModifierChange(setTo.width, setTo.height);
if (duration === 0) {
this.waScaleManager.zoomModifier = currentZoomModifier + zoomModifierChange;
this.camera.centerOn(setTo.x, setTo.y);
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
this.playerToFollow?.once(hasMovedEventName, () => {
if (this.playerToFollow) {
this.startFollowPlayer(this.playerToFollow, duration);
}
});
return;
}
this.camera.pan(setTo.x, setTo.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => {
if (this.cameraMode === CameraMode.Positioned) {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
}
if (progress === 1) {
this.playerToFollow?.once(hasMovedEventName, () => {
if (this.playerToFollow) {
this.startFollowPlayer(this.playerToFollow, duration);
}
});
}
});
}
/**
* Set camera to focus mode. As long as the camera is in the Focus mode, its view cannot be changed.
* @param setTo Viewport on which the camera should focus on
* @param duration Time for the transition im MS. If set to 0, transition will occur immediately
*/
public enterFocusMode(focusOn: WaScaleManagerFocusTarget, margin: number = 0, duration: number = 1000): void {
this.setCameraMode(CameraMode.Focus);
this.waScaleManager.saveZoom();
this.waScaleManager.setFocusTarget(focusOn);
this.cameraLocked = true;
this.unlockCameraWithDelay(duration);
this.restoreZoomTween?.stop();
this.startFollowTween?.stop();
this.camera.stopFollow();
this.playerToFollow = undefined;
const currentZoomModifier = this.waScaleManager.zoomModifier;
const zoomModifierChange = this.getZoomModifierChange(focusOn.width, focusOn.height, 1 + margin);
if (duration === 0) {
this.waScaleManager.zoomModifier = currentZoomModifier + zoomModifierChange;
this.camera.centerOn(focusOn.x, focusOn.y);
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
return;
}
this.camera.pan(focusOn.x, focusOn.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
if (progress === 1) {
// NOTE: Making sure the last action will be centering after zoom change
this.camera.centerOn(focusOn.x, focusOn.y);
}
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
});
}
public leaveFocusMode(player: Player, duration = 0): void {
this.waScaleManager.setFocusTarget();
this.unlockCameraWithDelay(duration);
this.startFollowPlayer(player, duration);
this.restoreZoom(duration);
}
public startFollowPlayer(player: Player, duration: number = 0): void {
this.playerToFollow = player;
this.setCameraMode(CameraMode.Follow);
if (duration === 0) {
this.camera.startFollow(player, true);
return;
}
const oldPos = { x: this.camera.scrollX, y: this.camera.scrollY };
this.startFollowTween = this.scene.tweens.addCounter({
from: 0,
to: 1,
duration,
ease: Easing.SineEaseOut,
onUpdate: (tween: Phaser.Tweens.Tween) => {
if (!this.playerToFollow) {
return;
}
const shiftX =
(this.playerToFollow.x - this.camera.worldView.width * 0.5 - oldPos.x) * tween.getValue();
const shiftY =
(this.playerToFollow.y - this.camera.worldView.height * 0.5 - oldPos.y) * tween.getValue();
this.camera.setScroll(oldPos.x + shiftX, oldPos.y + shiftY);
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
},
onComplete: () => {
this.camera.startFollow(player, 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<HTMLCanvasElement>("#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
);
}
public isCameraLocked(): boolean {
return this.cameraLocked;
}
private getZoomModifierChange(width?: number, height?: number, multiplier: number = 1): number {
if (!width || !height) {
return 0;
}
const targetZoomModifier = this.waScaleManager.getTargetZoomModifierFor(
width * multiplier,
height * multiplier
);
const currentZoomModifier = this.waScaleManager.zoomModifier;
return targetZoomModifier - currentZoomModifier;
}
private unlockCameraWithDelay(delay: number): void {
this.scene.time.delayedCall(delay, () => {
this.cameraLocked = false;
});
}
private setCameraMode(mode: CameraMode): void {
if (this.cameraMode === mode) {
return;
}
this.cameraMode = mode;
}
private restoreZoom(duration: number = 0): void {
if (duration === 0) {
this.waScaleManager.zoomModifier = this.waScaleManager.getSaveZoom();
return;
}
this.restoreZoomTween?.stop();
this.restoreZoomTween = this.scene.tweens.addCounter({
from: this.waScaleManager.zoomModifier,
to: this.waScaleManager.getSaveZoom(),
duration,
ease: Easing.SineEaseOut,
onUpdate: (tween: Phaser.Tweens.Tween) => {
this.waScaleManager.zoomModifier = tween.getValue();
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
},
});
}
private initCamera() {
this.camera = this.scene.cameras.main;
this.camera.setBounds(0, 0, this.cameraBounds.x, this.cameraBounds.y);
}
private bindEventHandlers(): void {
this.scene.game.events.on(
WaScaleManagerEvent.RefreshFocusOnTarget,
(focusOn: { x: number; y: number; width: number; height: number }) => {
if (!focusOn) {
return;
}
this.camera.centerOn(focusOn.x, focusOn.y);
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
}
);
this.camera.on("followupdate", () => {
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
});
}
private getCameraUpdateEventData(): CameraManagerEventCameraUpdateData {
return {
x: this.camera.worldView.x,
y: this.camera.worldView.y,
width: this.camera.worldView.width,
height: this.camera.worldView.height,
zoom: this.camera.scaleManager.zoom,
};
}
}