Merge branch 'develop' of github.com:thecodingmachine/workadventure

This commit is contained in:
_Bastler
2021-10-29 19:57:51 +02:00
19 changed files with 1059 additions and 77 deletions
+11
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>;
@@ -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();
+3
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;
+19
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"],
@@ -401,6 +402,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({
+50
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",
+74 -7
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();
}
/**
@@ -1,15 +1,32 @@
import type { GameScene } from "./GameScene";
import type { GameMap } from "./GameMap";
import { scriptUtils } from "../../Api/ScriptUtils";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { get } from 'svelte/store';
import {
ON_ACTION_TRIGGER_BUTTON,
TRIGGER_WEBSITE_PROPERTIES,
WEBSITE_MESSAGE_PROPERTIES,
} from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap";
enum OpenCoWebsiteState {
LOADING,
OPENED,
MUST_BE_CLOSE,
}
interface OpenCoWebsite {
coWebsite: CoWebsite | undefined,
state: OpenCoWebsiteState
}
export class GameMapPropertiesListener {
private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>();
private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>();
constructor(private scene: GameScene, private gameMap: GameMap) {}
register() {
@@ -36,42 +53,178 @@ export class GameMapPropertiesListener {
}
}
});
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
const handler = async () => {
if (newValue === undefined || newValue !== oldValue) {
layoutManagerActionStore.removeAction("openWebsite");
await coWebsiteManager.closeCoWebsites();
}
if (newValue !== undefined) {
// Open a new co-website by the property.
this.gameMap.onEnterLayer((newLayers) => {
const handler = () => {
newLayers.forEach(layer => {
if (!layer.properties) {
return;
}
let openWebsiteProperty: string | undefined;
let allowApiProperty: boolean | undefined;
let websitePolicyProperty: string | undefined;
let websiteWidthProperty: number | undefined;
let websitePositionProperty: number | undefined;
let websiteTriggerProperty: string | undefined;
let websiteTriggerMessageProperty: string | undefined;
layer.properties.forEach(property => {
switch(property.name) {
case 'openWebsite':
openWebsiteProperty = property.value as string | undefined;
break;
case 'openWebsiteAllowApi':
allowApiProperty = property.value as boolean | undefined;
break;
case 'openWebsitePolicy':
websitePolicyProperty = property.value as string | undefined;
break;
case 'openWebsiteWidth':
websiteWidthProperty = property.value as number | undefined;
break;
case 'openWebsitePosition':
websitePositionProperty = property.value as number | undefined;
break;
case TRIGGER_WEBSITE_PROPERTIES:
websiteTriggerProperty = property.value as string | undefined;
break;
case WEBSITE_MESSAGE_PROPERTIES:
websiteTriggerMessageProperty = property.value as string | undefined;
break;
}
});
if (!openWebsiteProperty) {
return;
}
const actionUuid = "openWebsite-" + (Math.random() + 1).toString(36).substring(7);
if (this.coWebsitesOpenByLayer.has(layer)) {
return;
}
this.coWebsitesOpenByLayer.set(layer, {
coWebsite: undefined,
state: OpenCoWebsiteState.LOADING,
});
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(
newValue as string,
openWebsiteProperty as string,
this.scene.MapUrlFile,
allProps.get("openWebsiteAllowApi") as boolean | undefined,
allProps.get("openWebsitePolicy") as string | undefined,
allProps.get("openWebsiteWidth") as number | undefined
);
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
websitePositionProperty,
).then(coWebsite => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
coWebsiteManager.closeCoWebsite(coWebsite);
this.coWebsitesOpenByLayer.delete(layer);
this.coWebsitesActionTriggerByLayer.delete(layer);
} else {
this.coWebsitesOpenByLayer.set(layer, {
coWebsite,
state: OpenCoWebsiteState.OPENED
});
}
});
layoutManagerActionStore.removeAction("openWebsite");
layoutManagerActionStore.removeAction(actionUuid);
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES);
if (message === undefined) {
message = "Press SPACE or touch here to open web site";
if (websiteTriggerProperty && websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) {
if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site";
}
this.coWebsitesActionTriggerByLayer.set(layer, actionUuid);
layoutManagerActionStore.addAction({
uuid: "openWebsite",
uuid: actionUuid,
type: "message",
message: message,
message: websiteTriggerMessageProperty,
callback: () => openWebsiteFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openWebsiteFunction();
}
}
});
};
handler();
});
// Close opened co-websites on leave the layer who contain the property.
this.gameMap.onLeaveLayer((oldLayers) => {
const handler = () => {
oldLayers.forEach(layer => {
if (!layer.properties) {
return;
}
let openWebsiteProperty: string | undefined;
let websiteTriggerProperty: string | undefined;
layer.properties.forEach(property => {
switch(property.name) {
case 'openWebsite':
openWebsiteProperty = property.value as string | undefined;
break;
case TRIGGER_WEBSITE_PROPERTIES:
websiteTriggerProperty = property.value as string | undefined;
break;
}
});
if (!openWebsiteProperty) {
return;
}
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (!coWebsiteOpen) {
return;
}
if (coWebsiteOpen.state === OpenCoWebsiteState.LOADING) {
coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE;
return;
}
if (coWebsiteOpen.state !== OpenCoWebsiteState.OPENED) {
return;
}
if (coWebsiteOpen.coWebsite !== undefined) {
coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite);
}
this.coWebsitesOpenByLayer.delete(layer);
if (!websiteTriggerProperty) {
return;
}
const actionStore = get(layoutManagerActionStore);
const actionTriggerUuid = this.coWebsitesActionTriggerByLayer.get(layer);
if (!actionTriggerUuid) {
return;
}
const action = actionStore && actionStore.length > 0 ?
actionStore.find(action => action.uuid === actionTriggerUuid) : undefined;
if (action) {
layoutManagerActionStore.removeAction(actionTriggerUuid);
}
});
};
handler();
+21 -1
View File
@@ -188,6 +188,8 @@ export class GameScene extends DirtyScene {
moving: false,
x: -1000,
y: -1000,
oldX: -1000,
oldY: -1000,
};
private gameMap!: GameMap;
@@ -766,6 +768,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);
});
});
});
}
@@ -897,6 +912,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);
@@ -1787,7 +1803,11 @@ export class GameScene extends DirtyScene {
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);
+2
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,
};
+3 -3
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) {
+2
View File
@@ -9,7 +9,9 @@ export interface LayoutManagerAction {
userInputManager: UserInputManager | undefined;
}
function createLayoutManagerAction() {
const { subscribe, set, update } = writable<LayoutManagerAction[]>([]);
return {