merged develop

This commit is contained in:
Hanusiak Piotr 2021-12-15 15:23:21 +01:00
commit 99ffb7b450
177 changed files with 5904 additions and 2328 deletions

View file

@ -1,6 +1,9 @@
import Sprite = Phaser.GameObjects.Sprite;
import Container = Phaser.GameObjects.Container;
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
import { TexturesHelper } from "../Helpers/TexturesHelper";
import { Writable, writable } from "svelte/store";
import type { PictureStore } from "../../Stores/PictureStore";
export interface CompanionStatus {
x: number;
@ -21,6 +24,7 @@ export class Companion extends Container {
private companionName: string;
private direction: PlayerAnimationDirections;
private animationType: PlayerAnimationTypes;
private readonly _pictureStore: Writable<string | undefined>;
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
super(scene, x + 14, y + 4);
@ -35,10 +39,14 @@ export class Companion extends Container {
this.animationType = PlayerAnimationTypes.Idle;
this.companionName = name;
this._pictureStore = writable(undefined);
texturePromise.then((resource) => {
this.addResource(resource);
this.invisible = false;
return this.getSnapshot().then((htmlImageElementSrc) => {
this._pictureStore.set(htmlImageElementSrc);
});
});
this.scene.physics.world.enableBody(this);
@ -123,6 +131,22 @@ export class Companion extends Container {
};
}
public async getSnapshot(): Promise<string> {
const sprites = Array.from(this.sprites.values()).map((sprite) => {
return { sprite, frame: 1 };
});
return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => {
console.warn(reason);
for (const sprite of this.sprites.values()) {
// it can be either cat or dog prefix
if (sprite.texture.key.includes("cat") || sprite.texture.key.includes("dog")) {
return this.scene.textures.getBase64(sprite.texture.key);
}
}
return "cat1";
});
}
private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void {
if (this.invisible) return;
@ -220,4 +244,8 @@ export class Companion extends Container {
super.destroy();
}
public get pictureStore(): PictureStore {
return this._pictureStore;
}
}

View file

@ -8,10 +8,12 @@ import { TextureError } from "../../Exception/TextureError";
import { Companion } from "../Companion/Companion";
import type { GameScene } from "../Game/GameScene";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import { waScaleManager } from "../Services/WaScaleManager";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { isSilentStore } from "../../Stores/MediaStore";
import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager";
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
import { TexturesHelper } from "../Helpers/TexturesHelper";
import type { PictureStore } from "../../Stores/PictureStore";
import { Writable, writable } from "svelte/store";
const playerNameY = -25;
@ -37,6 +39,7 @@ export abstract class Character extends Container {
private emote: Phaser.GameObjects.DOMElement | null = null;
private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene;
private readonly _pictureStore: Writable<string | undefined>;
constructor(
scene: GameScene,
@ -57,6 +60,7 @@ export abstract class Character extends Container {
this.invisible = true;
this.sprites = new Map<string, Sprite>();
this._pictureStore = writable(undefined);
//textures are inside a Promise in case they need to be lazyloaded before use.
texturesPromise
@ -64,6 +68,9 @@ export abstract class Character extends Container {
this.addTextures(textures, frame);
this.invisible = false;
this.playAnimation(direction, moving);
return this.getSnapshot().then((htmlImageElementSrc) => {
this._pictureStore.set(htmlImageElementSrc);
});
})
.catch(() => {
return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => {
@ -117,8 +124,20 @@ export abstract class Character extends Container {
}
}
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
private async getSnapshot(): Promise<string> {
const sprites = Array.from(this.sprites.values()).map((sprite) => {
return { sprite, frame: 1 };
});
return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => {
console.warn(reason);
for (const sprite of this.sprites.values()) {
// we can be sure that either predefined woka or body texture is at this point loaded
if (sprite.texture.key.includes("color") || sprite.texture.key.includes("male")) {
return this.scene.textures.getBase64(sprite.texture.key);
}
}
return "male1";
});
}
public addCompanion(name: string, texturePromise?: Promise<string>): void {
@ -154,6 +173,10 @@ export abstract class Character extends Container {
}
}
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
}
private getPlayerAnimations(name: string): AnimationData[] {
return [
{
@ -374,4 +397,8 @@ export abstract class Character extends Container {
this.emote = null;
this.playerName.setVisible(true);
}
public get pictureStore(): PictureStore {
return this._pictureStore;
}
}

View file

@ -0,0 +1,178 @@
import { Easing } from "../../types";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type { Box } from "../../WebRtc/LayoutManager";
import type { Player } from "../Player/Player";
import type { WaScaleManager } from "../Services/WaScaleManager";
import type { GameScene } from "./GameScene";
export enum CameraMode {
Free = "Free",
Follow = "Follow",
Focus = "Focus",
}
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.Free;
private restoreZoomTween?: Phaser.Tweens.Tween;
private startFollowTween?: Phaser.Tweens.Tween;
private cameraFollowTarget?: { 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();
this.bindEventHandlers();
}
public destroy(): void {
this.scene.game.events.off("wa-scale-manager:refresh-focus-on-target");
super.destroy();
}
public getCamera(): Phaser.Cameras.Scene2D.Camera {
return this.camera;
}
public enterFocusMode(
focusOn: { x: number; y: number; width: number; height: number },
margin: number = 0,
duration: number = 1000
): void {
this.setCameraMode(CameraMode.Focus);
this.waScaleManager.saveZoom();
this.waScaleManager.setFocusTarget(focusOn);
this.restoreZoomTween?.stop();
this.startFollowTween?.stop();
const marginMult = 1 + margin;
const targetZoomModifier = this.waScaleManager.getTargetZoomModifierFor(
focusOn.width * marginMult,
focusOn.height * marginMult
);
const currentZoomModifier = this.waScaleManager.zoomModifier;
const zoomModifierChange = targetZoomModifier - currentZoomModifier;
this.camera.stopFollow();
this.cameraFollowTarget = undefined;
this.camera.pan(
focusOn.x + focusOn.width * 0.5 * marginMult,
focusOn.y + focusOn.height * 0.5 * marginMult,
duration,
Easing.SineEaseOut,
true,
(camera, progress, x, y) => {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
}
);
}
public leaveFocusMode(player: Player): void {
this.waScaleManager.setFocusTarget();
this.startFollow(player, 1000);
this.restoreZoom(1000);
}
public startFollow(target: object | Phaser.GameObjects.GameObject, duration: number = 0): void {
this.cameraFollowTarget = target as { x: number; y: number };
this.setCameraMode(CameraMode.Follow);
if (duration === 0) {
this.camera.startFollow(target, 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.cameraFollowTarget) {
return;
}
const shiftX =
(this.cameraFollowTarget.x - this.camera.worldView.width * 0.5 - oldPos.x) * tween.getValue();
const shiftY =
(this.cameraFollowTarget.y - this.camera.worldView.height * 0.5 - oldPos.y) * tween.getValue();
this.camera.setScroll(oldPos.x + shiftX, oldPos.y + shiftY);
},
onComplete: () => {
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<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.cameraMode === CameraMode.Focus;
}
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();
},
});
}
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(
"wa-scale-manager:refresh-focus-on-target",
(focusOn: { x: number; y: number; width: number; height: number }) => {
if (!focusOn) {
return;
}
this.camera.centerOn(focusOn.x + focusOn.width * 0.5, focusOn.y + focusOn.height * 0.5);
}
);
}
}

View file

@ -1,8 +1,15 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapObjectLayer,
ITiledMapProperty,
} from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
import { GameMapProperties } from "./GameMapProperties";
import { MathUtils } from "../../Utils/MathUtils";
export type PropertyChangeCallback = (
newValue: string | number | boolean | undefined,
@ -15,24 +22,48 @@ export type layerChangeCallback = (
allLayersOnNewPosition: Array<ITiledMapLayer>
) => void;
export type zoneChangeCallback = (
zonesChangedByAction: Array<ITiledMapObject>,
allZonesOnNewPosition: Array<ITiledMapObject>
) => void;
/**
* A wrapper around a ITiledMap interface to provide additional capabilities.
* It is used to handle layer properties.
*/
export class GameMap {
// oldKey is the index of the previous tile.
/**
* oldKey is the index of the previous tile.
*/
private oldKey: number | undefined;
// key is the index of the current tile.
/**
* key is the index of the current tile.
*/
private key: number | undefined;
/**
* oldPosition is the previous position of the player.
*/
private oldPosition: { x: number; y: number } | undefined;
/**
* position is the current position of the player.
*/
private position: { x: number; y: number } | undefined;
private lastProperties = new Map<string, string | boolean | number>();
private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
private enterLayerCallbacks = Array<layerChangeCallback>();
private leaveLayerCallbacks = Array<layerChangeCallback>();
private enterZoneCallbacks = Array<zoneChangeCallback>();
private leaveZoneCallbacks = Array<zoneChangeCallback>();
private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
public readonly flatLayers: ITiledMapLayer[];
public readonly tiledObjects: ITiledMapObject[];
public readonly phaserLayers: TilemapLayer[] = [];
public readonly zones: ITiledMapObject[] = [];
public exitUrls: Array<string> = [];
@ -44,6 +75,9 @@ export class GameMap {
terrains: Array<Phaser.Tilemaps.Tileset>
) {
this.flatLayers = flattenGroupLayersMap(map);
this.tiledObjects = this.getObjectsFromLayers(this.flatLayers);
this.zones = this.tiledObjects.filter((object) => object.type === "zone");
let depth = -2;
for (const layer of this.flatLayers) {
if (layer.type === "tilelayer") {
@ -88,6 +122,10 @@ export class GameMap {
* This will trigger events if properties are changing.
*/
public setPosition(x: number, y: number) {
this.oldPosition = this.position;
this.position = { x, y };
this.triggerZonesChange();
this.oldKey = this.key;
const xMap = Math.floor(x / this.map.tilewidth);
@ -126,7 +164,7 @@ export class GameMap {
}
}
private triggerLayersChange() {
private triggerLayersChange(): void {
const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : [];
const layersByNewKey = this.key ? this.getLayersByKey(this.key) : [];
@ -155,6 +193,53 @@ export class GameMap {
}
}
/**
* We use Tiled Objects with type "zone" as zones with defined x, y, width and height for easier event triggering.
*/
private triggerZonesChange(): void {
const zonesByOldPosition = this.oldPosition
? this.zones.filter((zone) => {
if (!this.oldPosition) {
return false;
}
return MathUtils.isOverlappingWithRectangle(this.oldPosition, zone);
})
: [];
const zonesByNewPosition = this.position
? this.zones.filter((zone) => {
if (!this.position) {
return false;
}
return MathUtils.isOverlappingWithRectangle(this.position, zone);
})
: [];
const enterZones = new Set(zonesByNewPosition);
const leaveZones = new Set(zonesByOldPosition);
enterZones.forEach((zone) => {
if (leaveZones.has(zone)) {
leaveZones.delete(zone);
enterZones.delete(zone);
}
});
if (enterZones.size > 0) {
const zonesArray = Array.from(enterZones);
for (const callback of this.enterZoneCallbacks) {
callback(zonesArray, zonesByNewPosition);
}
}
if (leaveZones.size > 0) {
const zonesArray = Array.from(leaveZones);
for (const callback of this.leaveZoneCallbacks) {
callback(zonesArray, zonesByNewPosition);
}
}
}
public getCurrentProperties(): Map<string, string | boolean | number> {
return this.lastProperties;
}
@ -251,6 +336,20 @@ export class GameMap {
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);
}
@ -362,4 +461,17 @@ export class GameMap {
this.trigger(oldPropName, oldPropValue, undefined, emptyProps);
}
}
private getObjectsFromLayers(layers: ITiledMapLayer[]): ITiledMapObject[] {
const objects: ITiledMapObject[] = [];
const objectLayers = layers.filter((layer) => layer.type === "objectgroup");
for (const objectLayer of objectLayers) {
if (objectLayer.type === "objectgroup") {
objects.push(...objectLayer.objects);
}
}
return objects;
}
}

View file

@ -4,6 +4,7 @@ import { scriptUtils } from "../../Api/ScriptUtils";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap";
@ -33,7 +34,8 @@ export class GameMapPropertiesListener {
}
if (typeof newValue == "string" && newValue.length) {
const openWebsiteTriggerValue = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to open web site in new tab";
@ -135,7 +137,8 @@ export class GameMapPropertiesListener {
layoutManagerActionStore.removeAction(actionUuid);
};
if (websiteTriggerProperty && websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) {
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) {
if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site";
}

View file

@ -1,7 +1,54 @@
import type { Subscription } from "rxjs";
import AnimatedTiles from "phaser-animated-tiles";
import { Queue } from "queue-typescript";
import { get } from "svelte/store";
import { userMessageManager } from "../../Administration/UserMessageManager";
import { iframeListener } from "../../Api/IframeListener";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { urlManager } from "../../Url/UrlManager";
import { mediaManager } from "../../WebRtc/MediaManager";
import { UserInputManager } from "../UserInput/UserInputManager";
import { gameManager } from "./GameManager";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import { soundManager } from "./SoundManager";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { iframeListener } from "../../Api/IframeListener";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { TextureError } from "../../Exception/TextureError";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { Loader } from "../Components/Loader";
import { RemotePlayer } from "../Entity/RemotePlayer";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { StartPositionCalculator } from "./StartPositionCalculator";
import { PropertyUtils } from "../Map/PropertyUtils";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { GameMapProperties } from "./GameMapProperties";
import type {
GroupCreatedUpdatedMessageInterface,
MessageUserJoined,
@ -12,86 +59,37 @@ import type {
PositionInterface,
RoomJoinedMessageInterface,
} from "../../Connexion/ConnexionModels";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { Queue } from "queue-typescript";
import { Box, ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { urlManager } from "../../Url/UrlManager";
import { TextureError } from "../../Exception/TextureError";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { Loader } from "../Components/Loader";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { UserInputManager } from "../UserInput/UserInputManager";
import type { AddPlayerInterface } from "./AddPlayerInterface";
import { gameManager } from "./GameManager";
import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { CameraManager } 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";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { playersStore } from "../../Stores/PlayersStore";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement;
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles";
import { StartPositionCalculator } from "./StartPositionCalculator";
import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import {
audioManagerFileStore,
audioManagerVisibilityStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import { PropertyUtils } from "../Map/PropertyUtils";
import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { get } from "svelte/store";
import { contactPageStore } from "../../Stores/MenuStore";
import { GameMapProperties } from "./GameMapProperties";
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
import Camera = Phaser.Cameras.Scene2D.Camera;
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@ -131,7 +129,7 @@ export class GameScene extends DirtyScene {
Terrains: Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey: Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
MapPlayersByKey: MapStore<number, RemotePlayer> = new MapStore<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap;
Objects!: Array<Phaser.Physics.Arcade.Sprite>;
mapFile!: ITiledMap;
@ -200,6 +198,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;
@ -273,7 +272,9 @@ export class GameScene extends DirtyScene {
// 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP.
// So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes)
// See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure
const url = new URL(file.src);
const base = new URL(window.location.href);
base.pathname = "";
const url = new URL(file.src, base.toString());
const host = url.host.split(":")[0];
if (
window.location.protocol === "https:" &&
@ -326,7 +327,6 @@ export class GameScene extends DirtyScene {
this.onMapLoad(data);
}
this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
//eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.load as any).rexWebFont({
custom: {
@ -555,7 +555,13 @@ 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));
@ -596,7 +602,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();
@ -649,7 +655,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(
@ -671,7 +677,6 @@ export class GameScene extends DirtyScene {
this.connection = onConnect.connection;
playersStore.connectToRoomConnection(this.connection);
userIsAdminStore.set(this.connection.hasTag("admin"));
this.connection.onUserJoins((message: MessageUserJoined) => {
@ -784,6 +789,42 @@ export class GameScene extends DirtyScene {
iframeListener.sendLeaveLayerEvent(layer.name);
});
});
this.gameMap.onEnterZone((zones) => {
for (const zone of zones) {
const focusable = zone.properties?.find((property) => property.name === "focusable");
if (focusable && focusable.value === true) {
const zoomMargin = zone.properties?.find((property) => property.name === "zoom_margin");
this.cameraManager.enterFocusMode(
zone,
zoomMargin ? Math.max(0, Number(zoomMargin.value)) : undefined
);
break;
}
}
zones.forEach((zone) => {
iframeListener.sendEnterZoneEvent(zone.name);
});
});
this.gameMap.onLeaveZone((zones) => {
for (const zone of zones) {
const focusable = zone.properties?.find((property) => property.name === "focusable");
if (focusable && focusable.value === true) {
this.cameraManager.leaveFocusMode(this.CurrentPlayer);
break;
}
}
zones.forEach((zone) => {
iframeListener.sendLeaveZoneEvent(zone.name);
});
});
// this.gameMap.onLeaveLayer((layers) => {
// layers.forEach((layer) => {
// iframeListener.sendLeaveLayerEvent(layer.name);
// });
// });
});
}
@ -873,7 +914,8 @@ export class GameScene extends DirtyScene {
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to enter Jitsi Meet room";
@ -1202,6 +1244,7 @@ ${escapedMessage}
tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables,
playerVariables: localUserStore.getAllUserProperties(),
userRoomToken: this.connection ? this.connection.userRoomToken : "",
};
});
this.iframeSubscriptionList.push(
@ -1427,6 +1470,7 @@ ${escapedMessage}
this.userInputManager.destroy();
this.pinchManager?.destroy();
this.emoteManager.destroy();
this.cameraManager.destroy();
this.peerStoreUnsubscribe();
this.emoteUnsubscribe();
this.emoteMenuUnsubscribe();
@ -1459,7 +1503,7 @@ ${escapedMessage}
this.MapPlayers.remove(player);
});
this.MapPlayersByKey = new Map<number, RemotePlayer>();
this.MapPlayersByKey.clear();
}
private getExitUrl(layer: ITiledMapLayer): string | undefined {
@ -1517,13 +1561,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) {
@ -1917,23 +1954,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<HTMLCanvasElement>("#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(
@ -2002,6 +2022,9 @@ ${escapedMessage}
}
zoomByFactor(zoomFactor: number) {
if (this.cameraManager.isCameraLocked()) {
return;
}
waScaleManager.zoomModifier *= zoomFactor;
biggestAvailableAreaStore.recompute();
}

View file

@ -1,7 +1,7 @@
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { iframeListener } from "../../Api/IframeListener";
import type { GameMap } from "./GameMap";
import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap";
import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties";
import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent";

View file

@ -0,0 +1,34 @@
export class TexturesHelper {
public static async getSnapshot(
scene: Phaser.Scene,
...sprites: { sprite: Phaser.GameObjects.Sprite; frame?: string | number }[]
): Promise<string> {
const rt = scene.make.renderTexture({}, false);
try {
for (const { sprite, frame } of sprites) {
if (frame) {
sprite.setFrame(frame);
}
rt.draw(sprite, sprite.displayWidth * 0.5, sprite.displayHeight * 0.5);
}
return new Promise<string>((resolve, reject) => {
try {
rt.snapshot(
(url) => {
resolve((url as HTMLImageElement).src);
rt.destroy();
},
"image/png",
1
);
} catch (error) {
rt.destroy();
reject(error);
}
});
} catch (error) {
rt.destroy();
throw new Error("Could not get the snapshot");
}
}
}

View file

@ -1,5 +1,4 @@
import * as Phaser from "phaser";
import { Scene } from "phaser";
import Sprite = Phaser.GameObjects.Sprite;
import type { ITiledMapObject } from "../../Map/ITiledMap";
import type { ItemFactoryInterface } from "../ItemFactoryInterface";

View file

@ -3,6 +3,7 @@ import { Scene } from "phaser";
import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
export const EntrySceneName = "EntryScene";
@ -17,6 +18,14 @@ export class EntryScene extends Scene {
});
}
// From the very start, let's preload images used in the ReconnectingScene.
preload() {
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
gameManager
.init(this.scene)

View file

@ -4,6 +4,7 @@ import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text;
import ScenePlugin = Phaser.Scenes.ScenePlugin;
import { WAError } from "./WAError";
import Axios from "axios";
export const ErrorSceneName = "ErrorScene";
enum Textures {
@ -36,7 +37,11 @@ export class ErrorScene extends Phaser.Scene {
preload() {
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
if (!this.cache.bitmapFont.has("main_font")) {
// We put this inside a "if" because despite the cache, Phaser will make a query to the XML file. And if there is no connection (which
// is not unlikely given the fact we are in an error scene), this will cause an error.
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
}
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
@ -71,9 +76,9 @@ export class ErrorScene extends Phaser.Scene {
/**
* Displays the error page, with an error message matching the "error" parameters passed in.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static showError(error: any, scene: ScenePlugin): void {
public static showError(error: unknown, scene: ScenePlugin): void {
console.error(error);
console.trace();
if (typeof error === "string" || error instanceof String) {
scene.start(ErrorSceneName, {
@ -86,9 +91,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: error.subTitle,
message: error.details,
});
} else if (error.response) {
} else if (Axios.isAxiosError(error) && error.response) {
// Axios HTTP error
// client received an error response (5xx, 4xx)
console.error("Axios error. Request:", error.request, " - Response: ", error.response);
scene.start(ErrorSceneName, {
title:
"HTTP " +
@ -98,9 +104,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: "An error occurred while accessing URL:",
message: error.response.config.url,
});
} else if (error.request) {
} else if (Axios.isAxiosError(error)) {
// Axios HTTP error
// client never received a response, or request never left
console.error("Axios error. No full HTTP response received. Request to URL:", error.config.url);
scene.start(ErrorSceneName, {
title: "Network error",
subTitle: error.message,

View file

@ -3,7 +3,7 @@ import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
export const ReconnectingSceneName = "ReconnectingScene";
enum ReconnectingTextures {
export enum ReconnectingTextures {
icon = "icon",
mainFont = "main_font",
}
@ -41,7 +41,7 @@ export class ReconnectingScene extends Phaser.Scene {
"Connection lost. Reconnecting..."
);
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
this.anims.create({
key: "right",
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),

View file

@ -94,7 +94,7 @@ export class HdpiManager {
/**
* We only accept integer but we make an exception for 1.5
*/
private getOptimalZoomLevel(realPixelNumber: number): number {
public getOptimalZoomLevel(realPixelNumber: number): number {
const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber);
if (1.5 <= result && result < 2) {
return 1.5;

View file

@ -5,13 +5,15 @@ 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;
private actualZoom: number = 1;
private _saveZoom: number = 1;
private focusTarget?: { x: number; y: number; width: number; height: number };
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
}
@ -23,18 +25,14 @@ class WaScaleManager {
public applyNewSize() {
const { width, height } = coWebsiteManager.getGameSize();
let devicePixelRatio = 1;
if (window.devicePixelRatio) {
devicePixelRatio = window.devicePixelRatio;
}
const devicePixelRatio = window.devicePixelRatio ?? 1;
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({
width: width * devicePixelRatio,
height: height * devicePixelRatio,
});
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height);
@ -59,6 +57,34 @@ class WaScaleManager {
this.game.markDirty();
}
/**
* Use this in case of resizing while focusing on something
*/
public refreshFocusOnTarget(): void {
if (!this.focusTarget) {
return;
}
this.zoomModifier = this.getTargetZoomModifierFor(this.focusTarget.width, this.focusTarget.height);
this.game.events.emit("wa-scale-manager:refresh-focus-on-target", this.focusTarget);
}
public setFocusTarget(targetDimensions?: { x: number; y: number; width: number; height: number }): void {
this.focusTarget = targetDimensions;
}
public getTargetZoomModifierFor(viewportWidth: number, viewportHeight: number) {
const { width: gameWidth, height: gameHeight } = coWebsiteManager.getGameSize();
const devicePixelRatio = window.devicePixelRatio ?? 1;
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({
width: gameWidth * devicePixelRatio,
height: gameHeight * devicePixelRatio,
});
const desiredZoom = Math.min(realSize.width / viewportWidth, realSize.height / viewportHeight);
const realPixelNumber = gameWidth * devicePixelRatio * gameHeight * devicePixelRatio;
return desiredZoom / (this.hdpiManager.getOptimalZoomLevel(realPixelNumber) || 1);
}
public get zoomModifier(): number {
return this.hdpiManager.zoomModifier;
}
@ -72,6 +98,10 @@ class WaScaleManager {
this._saveZoom = this.hdpiManager.zoomModifier;
}
public getSaveZoom(): number {
return this._saveZoom;
}
public restoreZoom(): void {
this.hdpiManager.zoomModifier = this._saveZoom;
this.applyNewSize();