diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index 4375dca6..ad08448b 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -97,7 +97,7 @@ You need to wait for the end of the initialization before calling `WA.player.get ```typescript WA.onInit().then(() => { - console.log('Tags: ', WA.player.getPosition()); + console.log('Position: ', WA.player.getPosition()); }) ``` diff --git a/front/src/Api/Events/HasCameraMovedEvent.ts b/front/src/Api/Events/HasCameraMovedEvent.ts new file mode 100644 index 00000000..23f85385 --- /dev/null +++ b/front/src/Api/Events/HasCameraMovedEvent.ts @@ -0,0 +1,18 @@ +import * as tg from "generic-type-guard"; + +export const isHasCameraMovedEvent = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + width: tg.isNumber, + height: tg.isNumber, + }) + .get(); + +/** + * A message sent from the game to the iFrame to notify a movement from the camera. + */ + +export type HasCameraMovedEvent = tg.GuardedType; + +export type HasCameraMovedEventCallback = (event: HasCameraMovedEvent) => void; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 2744eaa9..5b1c0f02 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -32,6 +32,7 @@ import { isPlayerPropertyEvent } from "./PlayerPropertyEvent"; import type { ChangeZoneEvent } from "./ChangeZoneEvent"; import { isColorEvent } from "./ColorEvent"; import { isPlayerPosition } from "./PlayerPosition"; +import type { HasCameraMovedEvent } from "./HasCameraMovedEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -52,6 +53,7 @@ export type IframeEventMap = { displayBubble: null; removeBubble: null; onPlayerMove: undefined; + onCameraMove: undefined; showLayer: LayerEvent; hideLayer: LayerEvent; setProperty: SetPropertyEvent; @@ -84,6 +86,7 @@ export interface IframeResponseEventMap { leaveZoneEvent: ChangeZoneEvent; buttonClickedEvent: ButtonClickedEvent; hasPlayerMoved: HasPlayerMovedEvent; + hasCameraMoved: HasCameraMovedEvent; menuItemClicked: MenuItemClickedEvent; setVariable: SetVariableEvent; messageTriggered: MessageReferenceEvent; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 67b49344..093cacc5 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 { HasCameraMovedEvent } from "./Events/HasCameraMovedEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; type AnswererCallback = ( @@ -95,6 +96,7 @@ class IframeListener { private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; + private sendCameraMove: boolean = false; // Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904 private answerers: { @@ -226,6 +228,8 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type == "onPlayerMove") { this.sendPlayerMove = true; + } else if (payload.type == "onCameraMove") { + this.sendCameraMove = true; } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { this._setTilesStream.next(payload.data); } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { @@ -442,6 +446,15 @@ class IframeListener { } } + hasCameraMoved(event: HasCameraMovedEvent) { + if (this.sendCameraMove) { + this.postMessage({ + type: "hasCameraMoved", + data: event, + }); + } + } + sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ type: "buttonClickedEvent", diff --git a/front/src/Api/iframe/Room/EmbeddedWebsite.ts b/front/src/Api/iframe/Room/EmbeddedWebsite.ts index 1a2761bd..d9c2d986 100644 --- a/front/src/Api/iframe/Room/EmbeddedWebsite.ts +++ b/front/src/Api/iframe/Room/EmbeddedWebsite.ts @@ -13,7 +13,7 @@ export class EmbeddedWebsite { private _allowApi: boolean; private _position: Rectangle; private readonly origin: "map" | "player" | undefined; - private _scale: number | undefined; + private _scale: number; constructor(private config: CreateEmbeddedWebsiteEvent) { this.name = config.name; @@ -23,7 +23,7 @@ export class EmbeddedWebsite { this._allowApi = config.allowApi ?? false; this._position = config.position; this.origin = config.origin; - this._scale = config.scale; + this._scale = config.scale ?? 1; } public get url() { @@ -116,7 +116,7 @@ export class EmbeddedWebsite { }); } - public get scale() { + public get scale(): number { return this._scale; } diff --git a/front/src/Api/iframe/camera.ts b/front/src/Api/iframe/camera.ts new file mode 100644 index 00000000..e2fb258e --- /dev/null +++ b/front/src/Api/iframe/camera.ts @@ -0,0 +1,29 @@ +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; +import { Subject } from "rxjs"; +import type { HasCameraMovedEvent, HasCameraMovedEventCallback } from "../Events/HasCameraMovedEvent"; +import { apiCallback } from "./registeredCallbacks"; +import { isHasCameraMovedEvent } from "../Events/HasCameraMovedEvent"; + +const moveStream = new Subject(); + +export class WorkAdventureCameraCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + type: "hasCameraMoved", + typeChecker: isHasCameraMovedEvent, + callback: (payloadData) => { + moveStream.next(payloadData); + }, + }), + ]; + + onCameraMove(callback: HasCameraMovedEventCallback): void { + moveStream.subscribe(callback); + sendToWorkadventure({ + type: "onCameraMove", + data: null, + }); + } +} + +export default new WorkAdventureCameraCommands(); diff --git a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts index 99c4bf5f..36e6b305 100644 --- a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts +++ b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts @@ -30,6 +30,7 @@ export class EmbeddedWebsiteManager { height: rect["height"], }, origin: website.origin, + scale: website.scale, }; }); @@ -144,9 +145,9 @@ export class EmbeddedWebsiteManager { name, url, /*x, - y, - width, - height,*/ + y, + width, + height,*/ allow, allowApi, visible, diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 417fab75..9febe432 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -93,6 +93,8 @@ import { MapStore } from "../../Stores/Utils/MapStore"; import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb"; import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore"; import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator"; +import Camera = Phaser.Cameras.Scene2D.Camera; +import type { HasCameraMovedEvent } from "../../Api/Events/HasCameraMovedEvent"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -783,6 +785,17 @@ export class GameScene extends DirtyScene { this.gameMap.setPosition(event.x, event.y); }); + //listen event to share the actual worldView when the camera is updated + this.cameras.main.on("followupdate", (camera: Camera) => { + const worldView: HasCameraMovedEvent = { + x: camera.worldView.x, + y: camera.worldView.y, + width: camera.worldView.width, + height: camera.worldView.height, + }; + iframeListener.hasCameraMoved(worldView); + }); + // Set up variables manager this.sharedVariablesManager = new SharedVariablesManager( this.connection, diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f33bc8d7..6b3ec8c3 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -20,6 +20,7 @@ import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Sound } from "./Api/iframe/Sound/Sound"; import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution"; +import camera from "./Api/iframe/camera"; const globalState = createState("global"); @@ -46,6 +47,7 @@ const wa = { sound, room, player, + camera, state: globalState, onInit(): Promise {