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

This commit is contained in:
_Bastler
2022-01-03 19:10:22 +01:00
52 changed files with 1449 additions and 863 deletions
@@ -22,6 +22,8 @@ export const isEmbeddedWebsiteEvent = new tg.IsInterface()
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
})
.get();
@@ -35,6 +37,8 @@ export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
})
.get();
+1
View File
@@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface()
tags: tg.isArray(tg.isString),
variables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
playerVariables: tg.isObject,
})
.get();
/**
+8
View File
@@ -30,6 +30,8 @@ import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEv
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import { isColorEvent } from "./ColorEvent";
import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
import { isGetPropertyEvent } from "./GetPropertyEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
@@ -51,6 +53,7 @@ export type IframeEventMap = {
displayBubble: null;
removeBubble: null;
onPlayerMove: undefined;
onCameraUpdate: undefined;
showLayer: LayerEvent;
hideLayer: LayerEvent;
setProperty: SetPropertyEvent;
@@ -83,6 +86,7 @@ export interface IframeResponseEventMap {
leaveZoneEvent: ChangeZoneEvent;
buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent;
wasCameraUpdated: WasCameraUpdatedEvent;
menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent;
messageTriggered: MessageReferenceEvent;
@@ -166,6 +170,10 @@ export const iframeQueryMapTypeGuards = {
query: tg.isUndefined,
answer: tg.isUndefined,
},
getPlayerPosition: {
query: tg.isUndefined,
answer: isPlayerPosition,
},
};
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
+10
View File
@@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isPlayerPosition = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
})
.get();
export type PlayerPosition = tg.GuardedType<typeof isPlayerPosition>;
+1
View File
@@ -4,6 +4,7 @@ export const isSetVariableEvent = new tg.IsInterface()
.withProperties({
key: tg.isString,
value: tg.isUnknown,
target: tg.isSingletonStringUnion("global", "player"),
})
.get();
/**
@@ -0,0 +1,19 @@
import * as tg from "generic-type-guard";
export const isWasCameraUpdatedEvent = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
zoom: tg.isNumber,
})
.get();
/**
* A message sent from the game to the iFrame to notify a movement from the camera.
*/
export type WasCameraUpdatedEvent = tg.GuardedType<typeof isWasCameraUpdatedEvent>;
export type WasCameraUpdatedEventCallback = (event: WasCameraUpdatedEvent) => void;
+13
View File
@@ -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 { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (
@@ -88,6 +89,9 @@ class IframeListener {
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly _trackCameraUpdateStream: Subject<LoadSoundEvent> = new Subject();
public readonly trackCameraUpdateStream = this._trackCameraUpdateStream.asObservable();
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable();
@@ -229,6 +233,8 @@ class IframeListener {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "onCameraUpdate") {
this._trackCameraUpdateStream.next();
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
@@ -448,6 +454,13 @@ class IframeListener {
}
}
sendCameraUpdated(event: WasCameraUpdatedEvent) {
this.postMessage({
type: "wasCameraUpdated",
data: event,
});
}
sendButtonClickedEvent(popupId: number, buttonId: number, input : boolean, inputValue : string | null): void {
this.postMessage({
type: "buttonClickedEvent",
@@ -12,6 +12,8 @@ export class EmbeddedWebsite {
private _allow: string;
private _allowApi: boolean;
private _position: Rectangle;
private readonly origin: "map" | "player" | undefined;
private _scale: number;
constructor(private config: CreateEmbeddedWebsiteEvent) {
this.name = config.name;
@@ -20,6 +22,12 @@ export class EmbeddedWebsite {
this._allow = config.allow ?? "";
this._allowApi = config.allowApi ?? false;
this._position = config.position;
this.origin = config.origin;
this._scale = config.scale ?? 1;
}
public get url() {
return this._url;
}
public set url(url: string) {
@@ -33,6 +41,10 @@ export class EmbeddedWebsite {
});
}
public get visible() {
return this._visible;
}
public set visible(visible: boolean) {
this._visible = visible;
sendToWorkadventure({
@@ -44,6 +56,10 @@ export class EmbeddedWebsite {
});
}
public get x() {
return this._position.x;
}
public set x(x: number) {
this._position.x = x;
sendToWorkadventure({
@@ -55,6 +71,10 @@ export class EmbeddedWebsite {
});
}
public get y() {
return this._position.y;
}
public set y(y: number) {
this._position.y = y;
sendToWorkadventure({
@@ -66,6 +86,10 @@ export class EmbeddedWebsite {
});
}
public get width() {
return this._position.width;
}
public set width(width: number) {
this._position.width = width;
sendToWorkadventure({
@@ -77,6 +101,10 @@ export class EmbeddedWebsite {
});
}
public get height() {
return this._position.height;
}
public set height(height: number) {
this._position.height = height;
sendToWorkadventure({
@@ -87,4 +115,19 @@ export class EmbeddedWebsite {
},
});
}
public get scale(): number {
return this._scale;
}
public set scale(scale: number) {
this._scale = scale;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
scale: this._scale,
},
});
}
}
+29
View File
@@ -0,0 +1,29 @@
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { Subject } from "rxjs";
import type { WasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
import { apiCallback } from "./registeredCallbacks";
import { isWasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
const moveStream = new Subject<WasCameraUpdatedEvent>();
export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdventureCameraCommands> {
callbacks = [
apiCallback({
type: "wasCameraUpdated",
typeChecker: isWasCameraUpdatedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
},
}),
];
onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
sendToWorkadventure({
type: "onCameraUpdate",
data: null,
});
return moveStream;
}
}
export default new WorkAdventureCameraCommands();
+15
View File
@@ -3,6 +3,7 @@ import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events
import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
import { createState } from "./state";
const moveStream = new Subject<HasPlayerMovedEvent>();
@@ -31,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => {
};
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
readonly state = createState("player");
callbacks = [
apiCallback({
type: "hasPlayerMoved",
@@ -74,6 +77,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
return uuid;
}
async getPosition(): Promise<Position> {
return await queryWorkadventure({
type: "getPlayerPosition",
data: undefined,
});
}
get userRoomToken(): string | undefined {
if (userRoomToken === undefined) {
throw new Error(
@@ -102,4 +112,9 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
}
}
export type Position = {
x: number;
y: number;
};
export default new WorkadventurePlayerCommands();
+63 -55
View File
@@ -8,93 +8,101 @@ 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> {
private setVariableResolvers = new Subject<SetVariableEvent>();
private variables = new Map<string, unknown>();
private variableSubscribers = new Map<string, Subject<unknown>>();
constructor(private target: "global" | "player") {
super();
this.setVariableResolvers.subscribe((event) => {
const oldValue = this.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;
}*/
this.variables.set(event.key, event.value);
const subject = this.variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
}
callbacks = [
apiCallback({
type: "setVariable",
typeChecker: isSetVariableEvent,
callback: (payloadData) => {
setVariableResolvers.next(payloadData);
if (payloadData.target === this.target) {
this.setVariableResolvers.next(payloadData);
}
},
}),
];
// TODO: see how we can remove this method from types exposed to WA.state object
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 (!this.variables.has(name)) {
this.variables.set(name, value);
}
}
}
saveVariable(key: string, value: unknown): Promise<void> {
variables.set(key, value);
this.variables.set(key, value);
return queryWorkadventure({
type: "setVariable",
data: {
key,
value,
target: this.target,
},
});
}
loadVariable(key: string): unknown {
return variables.get(key);
return this.variables.get(key);
}
hasVariable(key: string): boolean {
return variables.has(key);
return this.variables.has(key);
}
onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key);
let subject = this.variableSubscribers.get(key);
if (subject === undefined) {
subject = new Subject<unknown>();
variableSubscribers.set(key, subject);
this.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;
},
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
export function createState(target: "global" | "player"): WorkadventureStateCommands & { [key: string]: unknown } {
return new Proxy(new WorkadventureStateCommands(target), {
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;
}
return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
export default proxyCommand;
},
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
return true;
}
return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
}
-4
View File
@@ -1,8 +1,4 @@
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { Sound } from "./Sound/Sound";
import { EmbeddedWebsite } from "./Room/EmbeddedWebsite";
import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent";