diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts
index 5efae800..8989df75 100644
--- a/back/src/Services/SocketManager.ts
+++ b/back/src/Services/SocketManager.ts
@@ -97,6 +97,7 @@ export class SocketManager {
}
const roomJoinedMessage = new RoomJoinedMessage();
roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
+ roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken());
for (const [itemId, item] of room.getItemsState().entries()) {
const itemStateMessage = new ItemStateMessage();
diff --git a/back/src/Services/VariablesManager.ts b/back/src/Services/VariablesManager.ts
index 00aac3dc..6ec1cc3a 100644
--- a/back/src/Services/VariablesManager.ts
+++ b/back/src/Services/VariablesManager.ts
@@ -1,12 +1,7 @@
/**
* Handles variables shared between the scripting API and the server.
*/
-import {
- ITiledMap,
- ITiledMapLayer,
- ITiledMapObject,
- ITiledMapObjectLayer,
-} from "@workadventure/tiled-map-type-guard/dist";
+import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist";
import { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient";
diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md
index 39a13d9e..9af0b1c2 100644
--- a/docs/maps/api-player.md
+++ b/docs/maps/api-player.md
@@ -58,6 +58,34 @@ WA.onInit().then(() => {
})
```
+### Get the user-room token of the player
+
+```
+WA.player.userRoomToken: string;
+```
+
+The user-room token is available from the `WA.player.userRoomToken` property.
+
+This token can be used by third party services to authenticate a player and prove that the player is in a given room.
+The token is generated by the administration panel linked to WorkAdventure. The token is a string and is depending on your implementation of the administration panel.
+In WorkAdventure SAAS version, the token is a JWT token that contains information such as the player's room ID and its associated membership ID.
+
+If you are using the self-hosted version of WorkAdventure and you developed your own administration panel, the token can be anything.
+By default, self-hosted versions of WorkAdventure don't come with an administration panel, so the token string will be empty.
+
+{.alert.alert-info}
+A typical use-case for the user-room token is providing logo upload capabilities in a map.
+The token can be used as a way to authenticate a WorkAdventure player and ensure he is indeed in the map and authorized to upload a logo.
+
+{.alert.alert-info}
+You need to wait for the end of the initialization before accessing `WA.player.userRoomToken`
+
+```typescript
+WA.onInit().then(() => {
+ console.log('Token: ', WA.player.userRoomToken);
+})
+```
+
### Listen to player movement
```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
diff --git a/docs/maps/camera.md b/docs/maps/camera.md
new file mode 100644
index 00000000..9e58fcad
--- /dev/null
+++ b/docs/maps/camera.md
@@ -0,0 +1,92 @@
+{.section-title.accent.text-primary}
+# Working with camera
+
+## Focusable Zones
+
+It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this:
+
+
+
+
+
+### Adding new **Focusable Zone**:
+
+1. Make sure you are editing an **Object Layer**
+
+
+
+
+
+2. Select **Insert Rectangle** tool
+
+
+
+
+
+3. Define new object wherever you want. For example, you can make your chilling room event cosier!
+
+
+
+
+
+4. Make sure your object is of type "zone"!
+
+
+
+
+
+5. Edit this new object and click on **Add Property**, like this:
+
+
+
+
+
+6. Add a **bool** property of name *focusable*:
+
+
+
+
+
+7. Make sure it's checked! :)
+
+
+
+
+
+All should be set up now and your new **Focusable Zone** should be working fine!
+
+### Defining custom zoom margin:
+
+If you want, you can add an additional property to control how much should the camera zoom onto focusable zone.
+
+1. Like before, click on **Add Property**
+
+
+
+
+
+2. Add a **float** property of name *zoom_margin*:
+
+
+
+
+
+2. Define how much (in percentage value) should the zoom be decreased:
+
+
+
+
+
+ For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300.
+
+ - No margin defined
+
+
+
+
+
+ - Margin set to **0.35**
+
+
+
+
\ No newline at end of file
diff --git a/docs/maps/images/camera/0_focusable_zone.png b/docs/maps/images/camera/0_focusable_zone.png
new file mode 100644
index 00000000..8b54f11f
Binary files /dev/null and b/docs/maps/images/camera/0_focusable_zone.png differ
diff --git a/docs/maps/images/camera/1_object_layer.png b/docs/maps/images/camera/1_object_layer.png
new file mode 100644
index 00000000..6f57d0ae
Binary files /dev/null and b/docs/maps/images/camera/1_object_layer.png differ
diff --git a/docs/maps/images/camera/2_rectangle_zone.png b/docs/maps/images/camera/2_rectangle_zone.png
new file mode 100644
index 00000000..9b0b9cda
Binary files /dev/null and b/docs/maps/images/camera/2_rectangle_zone.png differ
diff --git a/docs/maps/images/camera/3_define_new_zone.png b/docs/maps/images/camera/3_define_new_zone.png
new file mode 100644
index 00000000..226028eb
Binary files /dev/null and b/docs/maps/images/camera/3_define_new_zone.png differ
diff --git a/docs/maps/images/camera/4_add_zone_type.png b/docs/maps/images/camera/4_add_zone_type.png
new file mode 100644
index 00000000..0416d1e4
Binary files /dev/null and b/docs/maps/images/camera/4_add_zone_type.png differ
diff --git a/docs/maps/images/camera/5_click_add_property.png b/docs/maps/images/camera/5_click_add_property.png
new file mode 100644
index 00000000..9aa96a2f
Binary files /dev/null and b/docs/maps/images/camera/5_click_add_property.png differ
diff --git a/docs/maps/images/camera/6_add_focusable_prop.png b/docs/maps/images/camera/6_add_focusable_prop.png
new file mode 100644
index 00000000..3ba1b955
Binary files /dev/null and b/docs/maps/images/camera/6_add_focusable_prop.png differ
diff --git a/docs/maps/images/camera/7_make_sure_checked.png b/docs/maps/images/camera/7_make_sure_checked.png
new file mode 100644
index 00000000..7fbcdb89
Binary files /dev/null and b/docs/maps/images/camera/7_make_sure_checked.png differ
diff --git a/docs/maps/images/camera/8_add_zoom_margin.png b/docs/maps/images/camera/8_add_zoom_margin.png
new file mode 100644
index 00000000..8e3f5256
Binary files /dev/null and b/docs/maps/images/camera/8_add_zoom_margin.png differ
diff --git a/docs/maps/images/camera/9_optional_zoom_margin_defined.png b/docs/maps/images/camera/9_optional_zoom_margin_defined.png
new file mode 100644
index 00000000..8b41d7d0
Binary files /dev/null and b/docs/maps/images/camera/9_optional_zoom_margin_defined.png differ
diff --git a/docs/maps/images/camera/no_margin.png b/docs/maps/images/camera/no_margin.png
new file mode 100644
index 00000000..b8c9dd18
Binary files /dev/null and b/docs/maps/images/camera/no_margin.png differ
diff --git a/docs/maps/images/camera/with_margin.png b/docs/maps/images/camera/with_margin.png
new file mode 100644
index 00000000..ffd057ea
Binary files /dev/null and b/docs/maps/images/camera/with_margin.png differ
diff --git a/docs/maps/menu.php b/docs/maps/menu.php
index 0bf0a7f9..10a2f4c5 100644
--- a/docs/maps/menu.php
+++ b/docs/maps/menu.php
@@ -51,6 +51,12 @@ return [
'markdown' => 'maps.website-in-map',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md',
],
+ [
+ 'title' => 'Camera',
+ 'url' => '/map-building/camera.md',
+ 'markdown' => 'maps.camera',
+ 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/camera.md',
+ ],
[
'title' => 'Variables',
'url' => '/map-building/variables.md',
diff --git a/docs/maps/scripting.md b/docs/maps/scripting.md
index 8b11fe74..6da3ddbf 100644
--- a/docs/maps/scripting.md
+++ b/docs/maps/scripting.md
@@ -60,7 +60,7 @@ WA.chat.sendChatMessage('Hello world', 'Mr Robot');
The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.chat.sendChatMessage` opens the chat and adds a message in it.
-In your browser console, when you open the map, the chat message should be displayed right away.
+The message should be displayed in the chat history as soon as you enter the room.
## Adding a script in an iFrame
diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md
index 70581a57..6e84a251 100644
--- a/docs/maps/wa-maps.md
+++ b/docs/maps/wa-maps.md
@@ -98,13 +98,14 @@ The exception is the "collides" property that can only be set on tiles, but not
By setting properties on the map itself, you can help visitors know more about the creators of the map.
The following *map* properties are supported:
-* `mapName` (string)
-* `mapDescription` (string)
-* `mapCopyright` (string)
+* `mapName` (string): The name of your map
+* `mapLink` (string): A link to your map, for example a repository
+* `mapDescription` (string): A short description of your map
+* `mapCopyright` (string): Copyright notice
-And *each tileset* can also have a property called `tilesetCopyright` (string).
+Each *tileset* can also have a property called `tilesetCopyright` (string).
+If you are using audio files in your map, you can declare a layer property `audioCopyright` (string).
Resulting in a "credit" page in the menu looking like this:
{.document-img}
-
diff --git a/front/src/Api/Events/ChangeZoneEvent.ts b/front/src/Api/Events/ChangeZoneEvent.ts
new file mode 100644
index 00000000..e7ca3668
--- /dev/null
+++ b/front/src/Api/Events/ChangeZoneEvent.ts
@@ -0,0 +1,11 @@
+import * as tg from "generic-type-guard";
+
+export const isChangeZoneEvent = new tg.IsInterface()
+ .withProperties({
+ name: tg.isString,
+ })
+ .get();
+/**
+ * A message sent from the game to the iFrame when a user enters or leaves a zone.
+ */
+export type ChangeZoneEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts
index 112c2880..1f0f36ed 100644
--- a/front/src/Api/Events/GameStateEvent.ts
+++ b/front/src/Api/Events/GameStateEvent.ts
@@ -9,6 +9,7 @@ export const isGameStateEvent = new tg.IsInterface()
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),
variables: tg.isObject,
+ userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
})
.get();
/**
diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts
index 081008c4..c338ddbe 100644
--- a/front/src/Api/Events/IframeEvent.ts
+++ b/front/src/Api/Events/IframeEvent.ts
@@ -28,6 +28,7 @@ import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
+import type { ChangeZoneEvent } from "./ChangeZoneEvent";
export interface TypedMessageEvent extends MessageEvent {
data: T;
@@ -76,6 +77,8 @@ export interface IframeResponseEventMap {
leaveEvent: EnterLeaveEvent;
enterLayerEvent: ChangeLayerEvent;
leaveLayerEvent: ChangeLayerEvent;
+ enterZoneEvent: ChangeZoneEvent;
+ leaveZoneEvent: ChangeZoneEvent;
buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent;
menuItemClicked: MenuItemClickedEvent;
diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts
index 3db35984..67b49344 100644
--- a/front/src/Api/IframeListener.ts
+++ b/front/src/Api/IframeListener.ts
@@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
+import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
type AnswererCallback = (
query: IframeQueryMap[T]["query"],
@@ -414,6 +415,24 @@ class IframeListener {
});
}
+ sendEnterZoneEvent(zoneName: string) {
+ this.postMessage({
+ type: "enterZoneEvent",
+ data: {
+ name: zoneName,
+ } as ChangeZoneEvent,
+ });
+ }
+
+ sendLeaveZoneEvent(zoneName: string) {
+ this.postMessage({
+ type: "leaveZoneEvent",
+ data: {
+ name: zoneName,
+ } as ChangeZoneEvent,
+ });
+ }
+
hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) {
this.postMessage({
diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts
index 078a1926..c46f3fbc 100644
--- a/front/src/Api/iframe/player.ts
+++ b/front/src/Api/iframe/player.ts
@@ -20,6 +20,12 @@ export const setTags = (_tags: string[]) => {
let uuid: string | undefined;
+let userRoomToken: string | undefined;
+
+export const setUserRoomToken = (token: string | undefined) => {
+ userRoomToken = token;
+};
+
export const setUuid = (_uuid: string | undefined) => {
uuid = _uuid;
};
@@ -67,6 +73,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution
+ import { gameManager } from "../../Phaser/Game/GameManager";
+ import type { PictureStore } from "../../Stores/PictureStore";
+ import { onDestroy } from "svelte";
+
+ export let userId: number;
+ export let placeholderSrc: string;
+ export let width: string = "62px";
+ export let height: string = "62px";
+
+ const gameScene = gameManager.getCurrentGameScene();
+ let companionWokaPictureStore: PictureStore | undefined;
+ if (userId === -1) {
+ companionWokaPictureStore = gameScene.CurrentPlayer.companion?.pictureStore;
+ } else {
+ companionWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(
+ userId,
+ (item) => item.companion?.pictureStore
+ );
+ }
+
+ let src = placeholderSrc;
+
+ if (companionWokaPictureStore) {
+ const unsubscribe = companionWokaPictureStore.subscribe((source) => {
+ src = source ?? placeholderSrc;
+ });
+
+ onDestroy(unsubscribe);
+ }
+
+
+
+
+
diff --git a/front/src/Components/Menu/AboutRoomSubMenu.svelte b/front/src/Components/Menu/AboutRoomSubMenu.svelte
index 666183e0..2bbb4d3c 100644
--- a/front/src/Components/Menu/AboutRoomSubMenu.svelte
+++ b/front/src/Components/Menu/AboutRoomSubMenu.svelte
@@ -6,11 +6,14 @@
let expandedMapCopyright = false;
let expandedTilesetCopyright = false;
+ let expandedAudioCopyright = false;
let mapName: string = "";
+ let mapLink: string = "";
let mapDescription: string = "";
let mapCopyright: string = "The map creator did not declare a copyright for the map.";
let tilesetCopyright: string[] = [];
+ let audioCopyright: string[] = [];
onMount(() => {
if (gameScene.mapFile.properties !== undefined) {
@@ -18,6 +21,10 @@
if (propertyName !== undefined && typeof propertyName.value === "string") {
mapName = propertyName.value;
}
+ const propertyLink = gameScene.mapFile.properties.find((property) => property.name === "mapLink");
+ if (propertyLink !== undefined && typeof propertyLink.value === "string") {
+ mapLink = propertyLink.value;
+ }
const propertyDescription = gameScene.mapFile.properties.find(
(property) => property.name === "mapDescription"
);
@@ -36,7 +43,18 @@
(property) => property.name === "tilesetCopyright"
);
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") {
- tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
+ // Assignment needed to trigger Svelte's reactivity
+ tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value];
+ }
+ }
+ }
+
+ for (const layer of gameScene.mapFile.layers) {
+ if (layer.type && layer.type === "tilelayer" && layer.properties) {
+ const propertyAudioCopyright = layer.properties.find((property) => property.name === "audioCopyright");
+ if (propertyAudioCopyright !== undefined && typeof propertyAudioCopyright.value === "string") {
+ // Assignment needed to trigger Svelte's reactivity
+ audioCopyright = [...audioCopyright, propertyAudioCopyright.value];
}
}
}
@@ -48,6 +66,9 @@
(expandedMapCopyright = !expandedMapCopyright)}>
Copyrights of the map
@@ -60,8 +81,21 @@
{copyright}
{:else}
- The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those
- tilesets have no license.
+ The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets
+ have no license.
+
+ {/each}
+
+
(expandedAudioCopyright = !expandedAudioCopyright)}>
+ Copyrights of audio files
+
+
+ {#each audioCopyright as copyright}
+
{copyright}
+ {:else}
+
+ The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
+ have no license.
{/each}
diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte
index bf34658f..bb5a2df2 100644
--- a/front/src/Components/Menu/MenuIcon.svelte
+++ b/front/src/Components/Menu/MenuIcon.svelte
@@ -1,6 +1,6 @@
+
+
+
+
diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts
index 4a4eea6e..9e4025b1 100644
--- a/front/src/Connexion/RoomConnection.ts
+++ b/front/src/Connexion/RoomConnection.ts
@@ -68,6 +68,7 @@ export class RoomConnection implements RoomConnection {
private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
private closed: boolean = false;
private tags: string[] = [];
+ private _userRoomToken: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
@@ -211,6 +212,7 @@ export class RoomConnection implements RoomConnection {
this.userId = roomJoinedMessage.getCurrentuserid();
this.tags = roomJoinedMessage.getTagList();
+ this._userRoomToken = roomJoinedMessage.getUserroomtoken();
this.dispatch(EventMessage.CONNECT, {
connection: this,
@@ -713,4 +715,8 @@ export class RoomConnection implements RoomConnection {
public getAllTags(): string[] {
return this.tags;
}
+
+ public get userRoomToken(): string | undefined {
+ return this._userRoomToken;
+ }
}
diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts
index 75eb844f..80b0236e 100644
--- a/front/src/Phaser/Companion/Companion.ts
+++ b/front/src/Phaser/Companion/Companion.ts
@@ -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;
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise) {
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 {
+ 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;
+ }
}
diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts
index 1211a52d..2e0bd363 100644
--- a/front/src/Phaser/Entity/Character.ts
+++ b/front/src/Phaser/Entity/Character.ts
@@ -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;
constructor(
scene: GameScene,
@@ -57,6 +60,7 @@ export abstract class Character extends Container {
this.invisible = true;
this.sprites = new Map();
+ 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 {
+ 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): 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;
+ }
}
diff --git a/front/src/Phaser/Game/CameraManager.ts b/front/src/Phaser/Game/CameraManager.ts
new file mode 100644
index 00000000..19c4821a
--- /dev/null
+++ b/front/src/Phaser/Game/CameraManager.ts
@@ -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("#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);
+ }
+ );
+ }
+}
diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts
index 8fe0e329..6688acb8 100644
--- a/front/src/Phaser/Game/GameMap.ts
+++ b/front/src/Phaser/Game/GameMap.ts
@@ -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
) => void;
+export type zoneChangeCallback = (
+ zonesChangedByAction: Array,
+ allZonesOnNewPosition: Array
+) => 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();
private propertiesChangeCallbacks = new Map>();
+
private enterLayerCallbacks = Array();
private leaveLayerCallbacks = Array();
+ private enterZoneCallbacks = Array();
+ private leaveZoneCallbacks = Array();
+
private tileNameMap = new Map();
private tileSetPropertyMap: { [tile_index: number]: Array } = {};
public readonly flatLayers: ITiledMapLayer[];
+ public readonly tiledObjects: ITiledMapObject[];
public readonly phaserLayers: TilemapLayer[] = [];
+ public readonly zones: ITiledMapObject[] = [];
public exitUrls: Array = [];
@@ -44,6 +75,9 @@ export class GameMap {
terrains: Array
) {
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 {
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;
+ }
}
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index aba64202..ae89e2c3 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -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,84 +59,35 @@ 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 { 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 FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
+import { MapStore } from "../../Stores/Utils/MapStore";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@@ -129,7 +127,7 @@ export class GameScene extends DirtyScene {
Terrains: Array;
CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group;
- MapPlayersByKey: Map = new Map();
+ MapPlayersByKey: MapStore = new MapStore();
Map!: Phaser.Tilemaps.Tilemap;
Objects!: Array;
mapFile!: ITiledMap;
@@ -198,6 +196,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;
@@ -550,7 +549,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));
@@ -591,7 +596,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();
@@ -644,7 +649,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(
@@ -666,7 +671,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) => {
@@ -779,6 +783,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);
+ // });
+ // });
});
}
@@ -1167,6 +1207,7 @@ ${escapedMessage}
roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables,
+ userRoomToken: this.connection ? this.connection.userRoomToken : "",
};
});
this.iframeSubscriptionList.push(
@@ -1369,6 +1410,7 @@ ${escapedMessage}
this.userInputManager.destroy();
this.pinchManager?.destroy();
this.emoteManager.destroy();
+ this.cameraManager.destroy();
this.peerStoreUnsubscribe();
this.emoteUnsubscribe();
this.emoteMenuUnsubscribe();
@@ -1400,7 +1442,7 @@ ${escapedMessage}
this.MapPlayers.remove(player);
});
- this.MapPlayersByKey = new Map();
+ this.MapPlayersByKey.clear();
}
private getExitUrl(layer: ITiledMapLayer): string | undefined {
@@ -1458,13 +1500,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) {
@@ -1856,23 +1891,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(
@@ -1941,6 +1959,9 @@ ${escapedMessage}
}
zoomByFactor(zoomFactor: number) {
+ if (this.cameraManager.isCameraLocked()) {
+ return;
+ }
waScaleManager.zoomModifier *= zoomFactor;
biggestAvailableAreaStore.recompute();
}
diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts
index 8f913765..5b5867dc 100644
--- a/front/src/Phaser/Game/SharedVariablesManager.ts
+++ b/front/src/Phaser/Game/SharedVariablesManager.ts
@@ -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";
interface Variable {
diff --git a/front/src/Phaser/Helpers/TexturesHelper.ts b/front/src/Phaser/Helpers/TexturesHelper.ts
new file mode 100644
index 00000000..348e957a
--- /dev/null
+++ b/front/src/Phaser/Helpers/TexturesHelper.ts
@@ -0,0 +1,34 @@
+export class TexturesHelper {
+ public static async getSnapshot(
+ scene: Phaser.Scene,
+ ...sprites: { sprite: Phaser.GameObjects.Sprite; frame?: string | number }[]
+ ): Promise {
+ 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((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");
+ }
+ }
+}
diff --git a/front/src/Phaser/Items/Computer/computer.ts b/front/src/Phaser/Items/Computer/computer.ts
index 4665c546..41fb6fc4 100644
--- a/front/src/Phaser/Items/Computer/computer.ts
+++ b/front/src/Phaser/Items/Computer/computer.ts
@@ -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";
diff --git a/front/src/Phaser/Services/HdpiManager.ts b/front/src/Phaser/Services/HdpiManager.ts
index 116f6816..9c4e9af4 100644
--- a/front/src/Phaser/Services/HdpiManager.ts
+++ b/front/src/Phaser/Services/HdpiManager.ts
@@ -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;
diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts
index 5ceaeb71..447b6a1f 100644
--- a/front/src/Phaser/Services/WaScaleManager.ts
+++ b/front/src/Phaser/Services/WaScaleManager.ts
@@ -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();
diff --git a/front/src/Stores/PictureStore.ts b/front/src/Stores/PictureStore.ts
new file mode 100644
index 00000000..9908c942
--- /dev/null
+++ b/front/src/Stores/PictureStore.ts
@@ -0,0 +1,6 @@
+import type { Readable } from "svelte/store";
+
+/**
+ * A store that contains the player/companion avatar picture
+ */
+export type PictureStore = Readable;
diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts
index e6f5b1af..07c18b96 100644
--- a/front/src/Stores/PlayersStore.ts
+++ b/front/src/Stores/PlayersStore.ts
@@ -12,7 +12,7 @@ let idCount = 0;
function createPlayersStore() {
let players = new Map();
- const { subscribe, set, update } = writable(players);
+ const { subscribe, set, update } = writable