diff --git a/docs/maps/api-deprecated.md b/docs/maps/api-deprecated.md index f2b582a5..cc96e12a 100644 --- a/docs/maps/api-deprecated.md +++ b/docs/maps/api-deprecated.md @@ -14,6 +14,7 @@ The list of functions below is **deprecated**. You should not use those but. use - Method `WA.goToRoom` is deprecated. It has been renamed to `WA.nav.goToRoom`. - Method `WA.openCoWebSite` is deprecated. It has been renamed to `WA.nav.openCoWebSite`. - Method `WA.closeCoWebSite` is deprecated. It has been renamed to `WA.nav.closeCoWebSite`. +- Method `WA.closeCoWebsite` is deprecated. It has been renamed to `WA.nav.closeCoWebsite`. - Method `WA.openPopup` is deprecated. It has been renamed to `WA.ui.openPopup`. - Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`. - Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`. diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index ed73c32d..39a13d9e 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -68,7 +68,9 @@ The event has the following attributes : * **moving (boolean):** **true** when the current player is moving, **false** otherwise. * **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. * **x (number):** coordinate X of the current player. -* **y (number):** coordinate Y of the current player. +* **y (number):** coordinate Y of the current player. +* **oldX (number):** old coordinate X of the current player. +* **oldY (number):** old coordinate Y of the current player. **callback:** the function that will be called when the current player is moving. It contains the event. diff --git a/front/src/Api/Events/ChangeLayerEvent.ts b/front/src/Api/Events/ChangeLayerEvent.ts new file mode 100644 index 00000000..77ff8ede --- /dev/null +++ b/front/src/Api/Events/ChangeLayerEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isChangeLayerEvent = new tg.IsInterface() + .withProperties({ + name: tg.isString, + }) + .get(); +/** + * A message sent from the game to the iFrame when a user enters or leaves a layer. + */ +export type ChangeLayerEvent = tg.GuardedType; diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts index 87b45482..a3f1aa21 100644 --- a/front/src/Api/Events/HasPlayerMovedEvent.ts +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -6,6 +6,8 @@ export const isHasPlayerMovedEvent = new tg.IsInterface() moving: tg.isBoolean, x: tg.isNumber, y: tg.isNumber, + oldX: tg.isOptional(tg.isNumber), + oldY: tg.isOptional(tg.isNumber), }) .get(); diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 9e31b46c..abb492c5 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -29,6 +29,7 @@ import type { } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; +import type { ChangeLayerEvent } from "./ChangeLayerEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -75,6 +76,8 @@ export interface IframeResponseEventMap { userInputChat: UserInputChatEvent; enterEvent: EnterLeaveEvent; leaveEvent: EnterLeaveEvent; + enterLayerEvent: ChangeLayerEvent; + leaveLayerEvent: ChangeLayerEvent; buttonClickedEvent: ButtonClickedEvent; hasPlayerMoved: HasPlayerMovedEvent; menuItemClicked: MenuItemClickedEvent; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index caa59420..d626fc05 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -30,6 +30,7 @@ import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; +import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -395,6 +396,24 @@ class IframeListener { }); } + sendEnterLayerEvent(layerName: string) { + this.postMessage({ + type: "enterLayerEvent", + data: { + name: layerName, + } as ChangeLayerEvent, + }); + } + + sendLeaveLayerEvent(layerName: string) { + this.postMessage({ + type: "leaveLayerEvent", + data: { + name: layerName, + } as ChangeLayerEvent, + }); + } + hasPlayerMoved(event: HasPlayerMovedEvent) { if (this.sendPlayerMove) { this.postMessage({ diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index 22df49c9..cfa02807 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -1,6 +1,7 @@ import { Subject } from "rxjs"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; +import { ChangeLayerEvent, isChangeLayerEvent } from "../Events/ChangeLayerEvent"; import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; @@ -12,6 +13,9 @@ import website from "./website"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); +const enterLayerStreams: Map> = new Map>(); +const leaveLayerStreams: Map> = new Map>(); + interface TileDescriptor { x: number; y: number; @@ -47,8 +51,25 @@ export class WorkadventureRoomCommands extends IframeApiContribution { + enterLayerStreams.get(payloadData.name)?.next(); + }, + }), + apiCallback({ + type: "leaveLayerEvent", + typeChecker: isChangeLayerEvent, + callback: (payloadData) => { + leaveLayerStreams.get(payloadData.name)?.next(); + }, + }), ]; + /** + * @deprecated Use onEnterLayer instead + */ onEnterZone(name: string, callback: () => void): void { let subject = enterStreams.get(name); if (subject === undefined) { @@ -57,6 +78,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution void): void { let subject = leaveStreams.get(name); if (subject === undefined) { @@ -65,12 +90,35 @@ export class WorkadventureRoomCommands extends IframeApiContribution { + let subject = enterLayerStreams.get(layerName); + if (subject === undefined) { + subject = new Subject(); + enterLayerStreams.set(layerName, subject); + } + + return subject; + } + + onLeaveLayer(layerName: string): Subject { + let subject = leaveLayerStreams.get(layerName); + if (subject === undefined) { + subject = new Subject(); + leaveLayerStreams.set(layerName, subject); + } + + return subject; + } + showLayer(layerName: string): void { sendToWorkadventure({ type: "showLayer", data: { name: layerName } }); } + hideLayer(layerName: string): void { sendToWorkadventure({ type: "hideLayer", data: { name: layerName } }); } + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { sendToWorkadventure({ type: "setProperty", @@ -81,10 +129,12 @@ export class WorkadventureRoomCommands extends IframeApiContribution { const event = await queryWorkadventure({ type: "getMapData", data: undefined }); return event.data as ITiledMap; } + setTiles(tiles: TileDescriptor[]) { sendToWorkadventure({ type: "setTiles", diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 0360859b..cd11c179 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -2,6 +2,7 @@ import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiled import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; +import { iframeListener } from "../../Api/IframeListener"; export type PropertyChangeCallback = ( newValue: string | number | boolean | undefined, @@ -9,14 +10,25 @@ export type PropertyChangeCallback = ( allProps: Map ) => void; +export type layerChangeCallback = ( + layersChangedByAction: Array, + allLayersOnNewPosition: 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. + private oldKey: number | undefined; + // key is the index of the current tile. private key: number | undefined; private lastProperties = new Map(); - private callbacks = new Map>(); + private propertiesChangeCallbacks = new Map>(); + private enterLayerCallbacks = Array(); + private leaveLayerCallbacks = Array(); private tileNameMap = new Map(); private tileSetPropertyMap: { [tile_index: number]: Array } = {}; @@ -68,22 +80,32 @@ export class GameMap { return []; } + private getLayersByKey(key: number): Array { + return this.flatLayers.filter(flatLayer => flatLayer.type === 'tilelayer' && flatLayer.data[key] !== 0); + } + /** * Sets the position of the current player (in pixels) * This will trigger events if properties are changing. */ public setPosition(x: number, y: number) { + this.oldKey = this.key; + const xMap = Math.floor(x / this.map.tilewidth); const yMap = Math.floor(y / this.map.tileheight); const key = xMap + yMap * this.map.width; + if (key === this.key) { return; } + this.key = key; - this.triggerAll(); + + this.triggerAllProperties(); + this.triggerLayersChange(); } - private triggerAll(): void { + private triggerAllProperties(): void { const newProps = this.getProperties(this.key ?? 0); const oldProps = this.lastProperties; this.lastProperties = newProps; @@ -105,6 +127,36 @@ export class GameMap { } } + private triggerLayersChange() { + const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : []; + const layersByNewKey = this.key ? this.getLayersByKey(this.key) : []; + + const enterLayers = new Set(layersByNewKey); + const leaveLayers = new Set(layersByOldKey); + + enterLayers.forEach(layer => { + if (leaveLayers.has(layer)) { + leaveLayers.delete(layer); + enterLayers.delete(layer); + } + }); + + + if (enterLayers.size > 0) { + const layerArray = Array.from(enterLayers); + for (const callback of this.enterLayerCallbacks) { + callback(layerArray, layersByNewKey); + } + } + + if (leaveLayers.size > 0) { + const layerArray = Array.from(leaveLayers); + for (const callback of this.leaveLayerCallbacks) { + callback(layerArray, layersByNewKey); + } + } + } + public getCurrentProperties(): Map { return this.lastProperties; } @@ -167,7 +219,7 @@ export class GameMap { newValue: string | number | boolean | undefined, allProps: Map ) { - const callbacksArray = this.callbacks.get(propName); + const callbacksArray = this.propertiesChangeCallbacks.get(propName); if (callbacksArray !== undefined) { for (const callback of callbacksArray) { callback(newValue, oldValue, allProps); @@ -179,14 +231,28 @@ export class GameMap { * Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on. */ public onPropertyChange(propName: string, callback: PropertyChangeCallback) { - let callbacksArray = this.callbacks.get(propName); + let callbacksArray = this.propertiesChangeCallbacks.get(propName); if (callbacksArray === undefined) { callbacksArray = new Array(); - this.callbacks.set(propName, callbacksArray); + this.propertiesChangeCallbacks.set(propName, callbacksArray); } callbacksArray.push(callback); } + /** + * Registers a callback called when the user moves inside another layer. + */ + public onEnterLayer(callback: layerChangeCallback) { + this.enterLayerCallbacks.push(callback); + } + + /** + * Registers a callback called when the user moves outside another layer. + */ + public onLeaveLayer(callback: layerChangeCallback) { + this.leaveLayerCallbacks.push(callback); + } + public findLayer(layerName: string): ITiledMapLayer | undefined { return this.flatLayers.find((layer) => layer.name === layerName); } @@ -284,7 +350,8 @@ export class GameMap { } property.value = propertyValue; - this.triggerAll(); + this.triggerAllProperties(); + this.triggerLayersChange(); } /** diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d65c4d54..c8858ad3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -186,6 +186,8 @@ export class GameScene extends DirtyScene { moving: false, x: -1000, y: -1000, + oldX: -1000, + oldY: -1000, }; private gameMap!: GameMap; @@ -764,6 +766,19 @@ export class GameScene extends DirtyScene { //init user position and play trigger to check layers properties this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); + + // Init layer change listener + this.gameMap.onEnterLayer(layers => { + layers.forEach(layer => { + iframeListener.sendEnterLayerEvent(layer.name); + }); + }); + + this.gameMap.onLeaveLayer(layers => { + layers.forEach(layer => { + iframeListener.sendLeaveLayerEvent(layer.name); + }); + }); }); } @@ -895,6 +910,7 @@ export class GameScene extends DirtyScene { audioManagerVisibilityStore.set(!(newValue === undefined)); }); + // TODO: Legacy functionnality replace by layer change this.gameMap.onPropertyChange("zone", (newValue, oldValue) => { if (oldValue) { iframeListener.sendLeaveEvent(oldValue as string); @@ -1749,7 +1765,11 @@ ${escapedMessage} const playerMovement = new PlayerMovement( { x: player.x, y: player.y }, this.currentTick, - message.position, + { + ...message.position, + oldX: undefined, + oldY: undefined, + }, this.currentTick + POSITION_DELAY ); this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index c3daedad..7758f010 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -38,6 +38,8 @@ export class PlayerMovement { return { x, y, + oldX: this.startPosition.x, + oldY: this.startPosition.y, direction: this.endPosition.direction, moving: true, }; diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 3edcdcde..28a1d3bd 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -64,14 +64,14 @@ export class Player extends Character { if (x !== 0 || y !== 0) { this.move(x, y); - this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y }); + this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y, oldX: x, oldY: y }); } else if (this.wasMoving && moving) { // slow joystick movement this.move(0, 0); - this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y }); + this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y, oldX: x, oldY: y }); } else if (this.wasMoving && !moving) { this.stop(); - this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y }); + this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y, oldX: x, oldY: y }); } if (direction !== null) { diff --git a/front/src/Stores/LayoutManagerStore.ts b/front/src/Stores/LayoutManagerStore.ts index e92cd3c4..063d45a7 100644 --- a/front/src/Stores/LayoutManagerStore.ts +++ b/front/src/Stores/LayoutManagerStore.ts @@ -9,7 +9,9 @@ export interface LayoutManagerAction { userInputManager: UserInputManager | undefined; } + function createLayoutManagerAction() { + const { subscribe, set, update } = writable([]); return { diff --git a/front/tests/Phaser/Game/PlayerMovementTest.ts b/front/tests/Phaser/Game/PlayerMovementTest.ts index ce2e2767..4b9e8e99 100644 --- a/front/tests/Phaser/Game/PlayerMovementTest.ts +++ b/front/tests/Phaser/Game/PlayerMovementTest.ts @@ -7,7 +7,12 @@ describe("Interpolation / Extrapolation", () => { x: 100, y: 200 }, 42000, { - x: 200, y: 100, moving: true, direction: "up" + x: 200, + y: 100, + oldX: undefined, + oldY: undefined, + moving: true, + direction: "up" }, 42200 ); @@ -19,6 +24,8 @@ describe("Interpolation / Extrapolation", () => { expect(playerMovement.getPosition(42100)).toEqual({ x: 150, y: 150, + oldX: undefined, + oldY: undefined, direction: 'up', moving: true }); @@ -26,6 +33,8 @@ describe("Interpolation / Extrapolation", () => { expect(playerMovement.getPosition(42200)).toEqual({ x: 200, y: 100, + oldX: undefined, + oldY: undefined, direction: 'up', moving: true }); @@ -33,6 +42,8 @@ describe("Interpolation / Extrapolation", () => { expect(playerMovement.getPosition(42300)).toEqual({ x: 250, y: 50, + oldX: undefined, + oldY: undefined, direction: 'up', moving: true }); @@ -43,7 +54,12 @@ describe("Interpolation / Extrapolation", () => { x: 100, y: 200 }, 42000, { - x: 200, y: 100, moving: false, direction: "up" + x: 200, + y: 100, + oldX: undefined, + oldY: undefined, + moving: false, + direction: "up" }, 42200 ); @@ -51,6 +67,8 @@ describe("Interpolation / Extrapolation", () => { expect(playerMovement.getPosition(42300)).toEqual({ x: 200, y: 100, + oldX: undefined, + oldY: undefined, direction: 'up', moving: false }); @@ -61,7 +79,12 @@ describe("Interpolation / Extrapolation", () => { x: 100, y: 200 }, 42000, { - x: 200, y: 100, moving: false, direction: "up" + x: 200, + y: 100, + oldX: undefined, + oldY: undefined, + moving: false, + direction: "up" }, 42200 ); @@ -69,6 +92,8 @@ describe("Interpolation / Extrapolation", () => { expect(playerMovement.getPosition(42100)).toEqual({ x: 150, y: 150, + oldX: undefined, + oldY: undefined, direction: 'up', moving: true }); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 119596fd..1761f1bd 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -231,12 +231,12 @@ export class SocketManager implements ZoneEventListener { try { client.viewport = viewport; - const world = this.rooms.get(client.roomId); - if (!world) { + const room = this.rooms.get(client.roomId); + if (!room) { console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'"); return; } - world.setViewport(client, client.viewport); + room.setViewport(client, client.viewport); } catch (e) { console.error('An error occurred on "SET_VIEWPORT" event'); console.error(e);