Merging with develop
This commit is contained in:
parent
3d5c222957
commit
cdd61bdb2c
135 changed files with 4313 additions and 2128 deletions
|
@ -4,10 +4,11 @@ export const isGameStateEvent = new tg.IsInterface()
|
|||
.withProperties({
|
||||
roomId: tg.isString,
|
||||
mapUrl: tg.isString,
|
||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
||||
nickname: tg.isString,
|
||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
variables: tg.isObject,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import type { GameStateEvent } from "./GameStateEvent";
|
||||
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||
import type { ChatEvent } from "./ChatEvent";
|
||||
|
@ -9,7 +10,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
|||
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
||||
import type { DataLayerEvent } from "./DataLayerEvent";
|
||||
import type { MapDataEvent } from "./MapDataEvent";
|
||||
import type { LayerEvent } from "./LayerEvent";
|
||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||
|
@ -24,6 +25,11 @@ import type {
|
|||
triggerMessage,
|
||||
TriggerMessageEvent,
|
||||
} from "./ui/TriggerMessageEvent";
|
||||
import type { SetVariableEvent } from "./SetVariableEvent";
|
||||
import { isGameStateEvent } from "./GameStateEvent";
|
||||
import { isMapDataEvent } from "./MapDataEvent";
|
||||
import { isSetVariableEvent } from "./SetVariableEvent";
|
||||
import { isMessageReferenceEvent, isTriggerMessageEvent } from "./ui/TriggerMessageEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
|
@ -49,7 +55,6 @@ export type IframeEventMap = {
|
|||
showLayer: LayerEvent;
|
||||
hideLayer: LayerEvent;
|
||||
setProperty: SetPropertyEvent;
|
||||
getDataLayer: undefined;
|
||||
loadSound: LoadSoundEvent;
|
||||
playSound: PlaySoundEvent;
|
||||
stopSound: null;
|
||||
|
@ -75,8 +80,8 @@ export interface IframeResponseEventMap {
|
|||
leaveEvent: EnterLeaveEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
dataLayer: DataLayerEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
setVariable: SetVariableEvent;
|
||||
messageTriggered: MessageReferenceEvent;
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
|
@ -90,22 +95,40 @@ export const isIframeResponseEventWrapper = (event: {
|
|||
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
|
||||
|
||||
/**
|
||||
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
|
||||
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame.
|
||||
* Types are defined using Type guards that will actually bused to enforce and check types.
|
||||
*/
|
||||
export type IframeQueryMap = {
|
||||
export const iframeQueryMapTypeGuards = {
|
||||
getState: {
|
||||
query: undefined;
|
||||
answer: GameStateEvent;
|
||||
};
|
||||
query: tg.isUndefined,
|
||||
answer: isGameStateEvent,
|
||||
},
|
||||
getMapData: {
|
||||
query: tg.isUndefined,
|
||||
answer: isMapDataEvent,
|
||||
},
|
||||
setVariable: {
|
||||
query: isSetVariableEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
triggerMessage: {
|
||||
query: isTriggerMessageEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
removeTriggerMessage: {
|
||||
query: isMessageReferenceEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
};
|
||||
|
||||
[triggerMessage]: {
|
||||
query: TriggerMessageEvent;
|
||||
answer: void;
|
||||
};
|
||||
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
|
||||
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
|
||||
type UnknownToVoid<T> = undefined extends T ? void : T;
|
||||
|
||||
[removeTriggerMessage]: {
|
||||
query: MessageReferenceEvent;
|
||||
answer: void;
|
||||
export type IframeQueryMap = {
|
||||
[key in keyof IframeQueryMapTypeGuardsType]: {
|
||||
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
|
||||
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -119,8 +142,21 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
|||
query: IframeQuery<T>;
|
||||
}
|
||||
|
||||
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
|
||||
return type in iframeQueryMapTypeGuards;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === "string";
|
||||
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => {
|
||||
const type = event.type;
|
||||
if (typeof type !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (!isIframeQueryKey(type)) {
|
||||
return false;
|
||||
}
|
||||
return iframeQueryMapTypeGuards[type].query(event.data);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> =>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isDataLayerEvent = new tg.IsInterface()
|
||||
export const isMapDataEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
data: tg.isObject,
|
||||
})
|
||||
|
@ -9,4 +9,4 @@ export const isDataLayerEvent = new tg.IsInterface()
|
|||
/**
|
||||
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
|
||||
*/
|
||||
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;
|
||||
export type MapDataEvent = tg.GuardedType<typeof isMapDataEvent>;
|
|
@ -5,7 +5,7 @@ export const isSetTilesEvent = tg.isArray(
|
|||
.withProperties({
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
tile: tg.isUnion(tg.isNumber, tg.isString),
|
||||
tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull),
|
||||
layer: tg.isString,
|
||||
})
|
||||
.get()
|
||||
|
|
20
front/src/Api/Events/SetVariableEvent.ts
Normal file
20
front/src/Api/Events/SetVariableEvent.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
|
||||
|
||||
export const isSetVariableEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
key: tg.isString,
|
||||
value: tg.isUnknown,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to change the value of the property of the layer
|
||||
*/
|
||||
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>;
|
||||
|
||||
export const isSetVariableIframeEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
type: tg.isSingletonString("setVariable"),
|
||||
data: isSetVariableEvent,
|
||||
})
|
||||
.get();
|
|
@ -14,7 +14,6 @@ import {
|
|||
IframeErrorAnswerEvent,
|
||||
IframeEvent,
|
||||
IframeEventMap,
|
||||
IframeQuery,
|
||||
IframeQueryMap,
|
||||
IframeResponseEvent,
|
||||
IframeResponseEventMap,
|
||||
|
@ -29,22 +28,27 @@ import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
|||
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
||||
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
|
||||
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
import type { DataLayerEvent } from "./Events/DataLayerEvent";
|
||||
import type { MapDataEvent } from "./Events/MapDataEvent";
|
||||
import type { GameStateEvent } from "./Events/GameStateEvent";
|
||||
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
||||
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
||||
import type { SetVariableEvent } from "./Events/SetVariableEvent";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"]
|
||||
) => IframeQueryMap[T]["answer"] | Promise<IframeQueryMap[T]["answer"]>;
|
||||
query: IframeQueryMap[T]["query"],
|
||||
source: MessageEventSource | null
|
||||
) => IframeQueryMap[T]["answer"] | PromiseLike<IframeQueryMap[T]["answer"]>;
|
||||
|
||||
/**
|
||||
* Listens to messages from iframes and turn those messages into easy to use observables.
|
||||
* Also allows to send messages to those iframes.
|
||||
*/
|
||||
class IframeListener {
|
||||
private readonly _readyStream: Subject<HTMLIFrameElement> = new Subject();
|
||||
public readonly readyStream = this._readyStream.asObservable();
|
||||
|
||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||
public readonly chatStream = this._chatStream.asObservable();
|
||||
|
||||
|
@ -90,9 +94,6 @@ class IframeListener {
|
|||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
||||
|
||||
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
|
||||
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
||||
|
||||
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
|
||||
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
|
||||
|
||||
|
@ -116,16 +117,15 @@ class IframeListener {
|
|||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
private sendPlayerMove: 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: {
|
||||
[key in keyof IframeQueryMap]?: AnswererCallback<key>;
|
||||
[str in keyof IframeQueryMap]?: unknown;
|
||||
} = {};
|
||||
|
||||
init() {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
<T extends keyof IframeEventMap, U extends keyof IframeQueryMap>(
|
||||
message: TypedMessageEvent<IframeEvent<T | U>>
|
||||
) => {
|
||||
(message: MessageEvent<unknown>) => {
|
||||
// Do we trust the sender of this message?
|
||||
// Let's only accept messages from the iframe that are allowed.
|
||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||
|
@ -157,9 +157,9 @@ class IframeListener {
|
|||
|
||||
if (isIframeQueryWrapper(payload)) {
|
||||
const queryId = payload.id;
|
||||
const query = payload.query as IframeQuery<U>;
|
||||
const query = payload.query;
|
||||
|
||||
const answerer = this.answerers[query.type] as AnswererCallback<U> | undefined;
|
||||
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined;
|
||||
if (answerer === undefined) {
|
||||
const errorMsg =
|
||||
'The iFrame sent a message of type "' +
|
||||
|
@ -177,35 +177,43 @@ class IframeListener {
|
|||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(answerer(query.data))
|
||||
.then((value) => {
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
data: value,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
})
|
||||
.catch((reason) => {
|
||||
console.error("An error occurred while responding to an iFrame query.", reason);
|
||||
let reasonMsg: string;
|
||||
if (reason instanceof Error) {
|
||||
reasonMsg = reason.message;
|
||||
} else {
|
||||
reasonMsg = reason.toString();
|
||||
}
|
||||
const errorHandler = (reason: unknown) => {
|
||||
console.error("An error occurred while responding to an iFrame query.", reason);
|
||||
let reasonMsg: string = "";
|
||||
if (reason instanceof Error) {
|
||||
reasonMsg = reason.message;
|
||||
} else if (typeof reason === "object") {
|
||||
reasonMsg = reason ? reason.toString() : "";
|
||||
} else if (typeof reason === "string") {
|
||||
reasonMsg = reason;
|
||||
}
|
||||
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
error: reasonMsg,
|
||||
} as IframeErrorAnswerEvent,
|
||||
"*"
|
||||
);
|
||||
});
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
error: reasonMsg,
|
||||
} as IframeErrorAnswerEvent,
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
Promise.resolve(answerer(query.data, message.source))
|
||||
.then((value) => {
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
data: value,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
})
|
||||
.catch(errorHandler);
|
||||
} catch (reason) {
|
||||
errorHandler(reason);
|
||||
}
|
||||
} else if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
|
||||
this._showLayerStream.next(payload.data);
|
||||
|
@ -250,8 +258,6 @@ class IframeListener {
|
|||
this._removeBubbleStream.next();
|
||||
} else if (payload.type == "onPlayerMove") {
|
||||
this.sendPlayerMove = true;
|
||||
} else if (payload.type == "getDataLayer") {
|
||||
this._dataLayerChangeStream.next();
|
||||
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
||||
const data = payload.data.menutItem;
|
||||
// @ts-ignore
|
||||
|
@ -268,13 +274,6 @@ class IframeListener {
|
|||
);
|
||||
}
|
||||
|
||||
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
||||
this.postMessage({
|
||||
type: "dataLayer",
|
||||
data: dataLayerEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the passed iFrame to send/receive messages via the API.
|
||||
*/
|
||||
|
@ -414,6 +413,13 @@ class IframeListener {
|
|||
});
|
||||
}
|
||||
|
||||
setVariable(setVariableEvent: SetVariableEvent) {
|
||||
this.postMessage({
|
||||
type: "setVariable",
|
||||
data: setVariableEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
|
@ -431,17 +437,31 @@ class IframeListener {
|
|||
* @param key The "type" of the query we are answering
|
||||
* @param callback
|
||||
*/
|
||||
public registerAnswerer<T extends keyof IframeQueryMap, Guard extends tg.TypeGuard<IframeQueryMap[T]["query"]>>(
|
||||
key: T,
|
||||
callback: AnswererCallback<T>,
|
||||
typeChecker?: Guard
|
||||
): void {
|
||||
this.answerers[key] = callback as never;
|
||||
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T>): void {
|
||||
this.answerers[key] = callback;
|
||||
}
|
||||
|
||||
public unregisterAnswerer(key: keyof IframeQueryMap): void {
|
||||
delete this.answerers[key];
|
||||
}
|
||||
|
||||
dispatchVariableToOtherIframes(key: string, value: unknown, source: MessageEventSource | null) {
|
||||
// Let's dispatch the message to the other iframes
|
||||
for (const iframe of this.iframes) {
|
||||
if (iframe.contentWindow !== source) {
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: "setVariable",
|
||||
data: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const iframeListener = new IframeListener();
|
||||
|
|
|
@ -6,6 +6,24 @@ import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
|||
|
||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||
|
||||
let playerName: string | undefined;
|
||||
|
||||
export const setPlayerName = (name: string) => {
|
||||
playerName = name;
|
||||
};
|
||||
|
||||
let tags: string[] | undefined;
|
||||
|
||||
export const setTags = (_tags: string[]) => {
|
||||
tags = _tags;
|
||||
};
|
||||
|
||||
let uuid: string | undefined;
|
||||
|
||||
export const setUuid = (_uuid: string | undefined) => {
|
||||
uuid = _uuid;
|
||||
};
|
||||
|
||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
|
@ -24,6 +42,31 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
if (playerName === undefined) {
|
||||
throw new Error(
|
||||
"Player name not initialized yet. You should call WA.player.name within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return playerName;
|
||||
}
|
||||
|
||||
get tags(): string[] {
|
||||
if (tags === undefined) {
|
||||
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
get id(): string | undefined {
|
||||
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
|
||||
// while uuid could.
|
||||
if (playerName === undefined) {
|
||||
throw new Error("Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.");
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventurePlayerCommands();
|
||||
|
|
|
@ -1,56 +1,33 @@
|
|||
import { Subject } from "rxjs";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { isDataLayerEvent } from "../Events/DataLayerEvent";
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
import { isGameStateEvent } from "../Events/GameStateEvent";
|
||||
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
import type { DataLayerEvent } from "../Events/DataLayerEvent";
|
||||
import type { GameStateEvent } from "../Events/GameStateEvent";
|
||||
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const dataLayerResolver = new Subject<DataLayerEvent>();
|
||||
const stateResolvers = new Subject<GameStateEvent>();
|
||||
|
||||
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
||||
|
||||
interface Room {
|
||||
id: string;
|
||||
mapUrl: string;
|
||||
map: ITiledMap;
|
||||
startLayer: string | null;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string | undefined;
|
||||
nickName: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface TileDescriptor {
|
||||
x: number;
|
||||
y: number;
|
||||
tile: number | string;
|
||||
tile: number | string | null;
|
||||
layer: string;
|
||||
}
|
||||
|
||||
function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableDataPromise === undefined) {
|
||||
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
||||
}
|
||||
return immutableDataPromise;
|
||||
}
|
||||
let roomId: string | undefined;
|
||||
|
||||
function getDataLayer(): Promise<DataLayerEvent> {
|
||||
return new Promise<DataLayerEvent>((resolver, thrower) => {
|
||||
dataLayerResolver.subscribe(resolver);
|
||||
sendToWorkadventure({ type: "getDataLayer", data: null });
|
||||
});
|
||||
}
|
||||
export const setRoomId = (id: string) => {
|
||||
roomId = id;
|
||||
};
|
||||
|
||||
let mapURL: string | undefined;
|
||||
|
||||
export const setMapURL = (url: string) => {
|
||||
mapURL = url;
|
||||
};
|
||||
|
||||
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||
callbacks = [
|
||||
|
@ -68,13 +45,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||
leaveStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "dataLayer",
|
||||
typeChecker: isDataLayerEvent,
|
||||
callback: (payloadData) => {
|
||||
dataLayerResolver.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
|
@ -109,22 +79,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||
},
|
||||
});
|
||||
}
|
||||
getCurrentRoom(): Promise<Room> {
|
||||
return getGameState().then((gameState) => {
|
||||
return getDataLayer().then((mapJson) => {
|
||||
return {
|
||||
id: gameState.roomId,
|
||||
map: mapJson.data as ITiledMap,
|
||||
mapUrl: gameState.mapUrl,
|
||||
startLayer: gameState.startLayerName,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
getCurrentUser(): Promise<User> {
|
||||
return getGameState().then((gameState) => {
|
||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||
});
|
||||
async getTiledMap(): Promise<ITiledMap> {
|
||||
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
|
||||
return event.data as ITiledMap;
|
||||
}
|
||||
setTiles(tiles: TileDescriptor[]) {
|
||||
sendToWorkadventure({
|
||||
|
@ -132,6 +89,22 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||
data: tiles,
|
||||
});
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
if (roomId === undefined) {
|
||||
throw new Error("Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.");
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
||||
get mapURL(): string {
|
||||
if (mapURL === undefined) {
|
||||
throw new Error(
|
||||
"mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return mapURL;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
||||
|
|
90
front/src/Api/iframe/state.ts
Normal file
90
front/src/Api/iframe/state.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
|
||||
const setVariableResolvers = new Subject<SetVariableEvent>();
|
||||
const variables = new Map<string, unknown>();
|
||||
const variableSubscribers = new Map<string, Subject<unknown>>();
|
||||
|
||||
export const initVariables = (_variables: Map<string, unknown>): void => {
|
||||
for (const [name, value] of _variables.entries()) {
|
||||
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
|
||||
if (!variables.has(name)) {
|
||||
variables.set(name, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setVariableResolvers.subscribe((event) => {
|
||||
const oldValue = variables.get(event.key);
|
||||
// If we are setting the same value, no need to do anything.
|
||||
// No need to do this check since it is already performed in SharedVariablesManager
|
||||
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
|
||||
return;
|
||||
}*/
|
||||
|
||||
variables.set(event.key, event.value);
|
||||
const subject = variableSubscribers.get(event.key);
|
||||
if (subject !== undefined) {
|
||||
subject.next(event.value);
|
||||
}
|
||||
});
|
||||
|
||||
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "setVariable",
|
||||
typeChecker: isSetVariableEvent,
|
||||
callback: (payloadData) => {
|
||||
setVariableResolvers.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
saveVariable(key: string, value: unknown): Promise<void> {
|
||||
variables.set(key, value);
|
||||
return queryWorkadventure({
|
||||
type: "setVariable",
|
||||
data: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
loadVariable(key: string): unknown {
|
||||
return variables.get(key);
|
||||
}
|
||||
|
||||
onVariableChange(key: string): Observable<unknown> {
|
||||
let subject = variableSubscribers.get(key);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<unknown>();
|
||||
variableSubscribers.set(key, subject);
|
||||
}
|
||||
return subject.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
|
||||
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
|
||||
if (p in target) {
|
||||
return Reflect.get(target, p, receiver);
|
||||
}
|
||||
return target.loadVariable(p.toString());
|
||||
},
|
||||
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
|
||||
// Note: when using "set", there is no way to wait, so we ignore the return of the promise.
|
||||
// User must use WA.state.saveVariable to have error message.
|
||||
target.saveVariable(p.toString(), value);
|
||||
return true;
|
||||
},
|
||||
}) as WorkadventureStateCommands & { [key: string]: unknown };
|
||||
|
||||
export default proxyCommand;
|
Loading…
Add table
Add a link
Reference in a new issue