Implement on enter/leave layer events

This commit is contained in:
Alexis Faizeau 2021-10-29 16:44:51 +02:00
parent 0b08d9251e
commit 934e24f837
14 changed files with 222 additions and 18 deletions

View file

@ -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`.

View file

@ -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.

View file

@ -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<typeof isChangeLayerEvent>;

View file

@ -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();

View file

@ -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<T> 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;

View file

@ -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<T extends keyof IframeQueryMap> = (
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({

View file

@ -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<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const enterLayerStreams: Map<string, Subject<void>> = new Map<string, Subject<void>>();
const leaveLayerStreams: Map<string, Subject<void>> = new Map<string, Subject<void>>();
interface TileDescriptor {
x: number;
y: number;
@ -47,8 +51,25 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
leaveStreams.get(payloadData.name)?.next();
},
}),
apiCallback({
type: "enterLayerEvent",
typeChecker: isChangeLayerEvent,
callback: (payloadData: ChangeLayerEvent) => {
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<Workadventu
}
subject.subscribe(callback);
}
/**
* @deprecated Use onLeaveLayer instead
*/
onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name);
if (subject === undefined) {
@ -65,12 +90,35 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
}
subject.subscribe(callback);
}
onEnterLayer(layerName: string): Subject<void> {
let subject = enterLayerStreams.get(layerName);
if (subject === undefined) {
subject = new Subject<ChangeLayerEvent>();
enterLayerStreams.set(layerName, subject);
}
return subject;
}
onLeaveLayer(layerName: string): Subject<void> {
let subject = leaveLayerStreams.get(layerName);
if (subject === undefined) {
subject = new Subject<ChangeLayerEvent>();
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<Workadventu
},
});
}
async getTiledMap(): Promise<ITiledMap> {
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
return event.data as ITiledMap;
}
setTiles(tiles: TileDescriptor[]) {
sendToWorkadventure({
type: "setTiles",

View file

@ -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<string, string | boolean | number>
) => void;
export type layerChangeCallback = (
layersChangedByAction: Array<ITiledMapLayer>,
allLayersOnNewPosition: Array<ITiledMapLayer>,
) => 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<string, string | boolean | number>();
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
private enterLayerCallbacks = Array<layerChangeCallback>();
private leaveLayerCallbacks = Array<layerChangeCallback>();
private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
@ -68,22 +80,32 @@ export class GameMap {
return [];
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
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<string, string | boolean | number> {
return this.lastProperties;
}
@ -167,7 +219,7 @@ export class GameMap {
newValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) {
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<PropertyChangeCallback>();
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();
}
/**

View file

@ -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);

View file

@ -38,6 +38,8 @@ export class PlayerMovement {
return {
x,
y,
oldX: this.startPosition.x,
oldY: this.startPosition.y,
direction: this.endPosition.direction,
moving: true,
};

View file

@ -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) {

View file

@ -9,7 +9,9 @@ export interface LayoutManagerAction {
userInputManager: UserInputManager | undefined;
}
function createLayoutManagerAction() {
const { subscribe, set, update } = writable<LayoutManagerAction[]>([]);
return {

View file

@ -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
});

View file

@ -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);