Creates player state and uses it to get and set values from local storage

This commit is contained in:
Benedicte Quimbert 2021-10-25 14:43:36 +02:00
parent e025c1dc8e
commit 3490daed6b
9 changed files with 153 additions and 137 deletions

View File

@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface()
tags: tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
variables: tg.isObject, variables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
playerVariables: tg.isObject,
}) })
.get(); .get();
/** /**

View File

@ -1,13 +0,0 @@
import * as tg from "generic-type-guard";
export const isPlayerPropertyEvent = new tg.IsInterface()
.withProperties({
propertyName: tg.isString,
propertyValue: tg.isUnknown,
})
.get();
/**
* A message sent from the iFrame to set player-related properties.
*/
export type PlayerPropertyEvent = tg.GuardedType<typeof isPlayerPropertyEvent>;

View File

@ -4,6 +4,7 @@ export const isSetVariableEvent = new tg.IsInterface()
.withProperties({ .withProperties({
key: tg.isString, key: tg.isString,
value: tg.isUnknown, value: tg.isUnknown,
target: tg.isSingletonStringUnion("global", "player"),
}) })
.get(); .get();
/** /**

View File

@ -3,7 +3,7 @@ import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
import type { PlayerPropertyEvent } from "../Events/PlayerPropertyEvent"; import { createState } from "./state";
const moveStream = new Subject<HasPlayerMovedEvent>(); const moveStream = new Subject<HasPlayerMovedEvent>();
@ -32,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => {
}; };
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> { export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
readonly state = createState("player");
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: "hasPlayerMoved", type: "hasPlayerMoved",

View File

@ -8,75 +8,84 @@ import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
const setVariableResolvers = new Subject<SetVariableEvent>(); export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
const variables = new Map<string, unknown>(); private setVariableResolvers = new Subject<SetVariableEvent>();
const variableSubscribers = new Map<string, Subject<unknown>>(); private variables = new Map<string, unknown>();
private variableSubscribers = new Map<string, Subject<unknown>>();
export const initVariables = (_variables: Map<string, unknown>): void => { constructor(private target: "global" | "player") {
for (const [name, value] of _variables.entries()) { super();
// 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) => { this.setVariableResolvers.subscribe((event) => {
const oldValue = variables.get(event.key); const oldValue = this.variables.get(event.key);
// If we are setting the same value, no need to do anything. // 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 // No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) { /*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return; return;
}*/ }*/
variables.set(event.key, event.value); this.variables.set(event.key, event.value);
const subject = variableSubscribers.get(event.key); const subject = this.variableSubscribers.get(event.key);
if (subject !== undefined) { if (subject !== undefined) {
subject.next(event.value); subject.next(event.value);
} }
}); });
}
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
callbacks = [ callbacks = [
apiCallback({ apiCallback({
type: "setVariable", type: "setVariable",
typeChecker: isSetVariableEvent, typeChecker: isSetVariableEvent,
callback: (payloadData) => { 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> { saveVariable(key: string, value: unknown): Promise<void> {
variables.set(key, value); this.variables.set(key, value);
return queryWorkadventure({ return queryWorkadventure({
type: "setVariable", type: "setVariable",
data: { data: {
key, key,
value, value,
target: this.target,
}, },
}); });
} }
loadVariable(key: string): unknown { loadVariable(key: string): unknown {
return variables.get(key); return this.variables.get(key);
} }
hasVariable(key: string): boolean { hasVariable(key: string): boolean {
return variables.has(key); return this.variables.has(key);
} }
onVariableChange(key: string): Observable<unknown> { onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key); let subject = this.variableSubscribers.get(key);
if (subject === undefined) { if (subject === undefined) {
subject = new Subject<unknown>(); subject = new Subject<unknown>();
variableSubscribers.set(key, subject); this.variableSubscribers.set(key, subject);
} }
return subject.asObservable(); return subject.asObservable();
} }
} }
const proxyCommand = new Proxy(new WorkadventureStateCommands(), { export function createState(target: "global" | "player"): WorkadventureStateCommands & { [key: string]: unknown } {
return new Proxy(new WorkadventureStateCommands(target), {
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
if (p in target) { if (p in target) {
return Reflect.get(target, p, receiver); return Reflect.get(target, p, receiver);
@ -96,5 +105,4 @@ const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
return target.hasVariable(p.toString()); return target.hasVariable(p.toString());
}, },
}) as WorkadventureStateCommands & { [key: string]: unknown }; }) as WorkadventureStateCommands & { [key: string]: unknown };
}
export default proxyCommand;

View File

@ -220,12 +220,26 @@ class LocalUserStore {
const cameraSetupValues = localStorage.getItem(cameraSetup); const cameraSetupValues = localStorage.getItem(cameraSetup);
return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined;
} }
getUserProperty(name: string): string | null {
return localStorage.getItem(userProperties + "_" + name); getAllUserProperties(): Map<string, unknown> {
const result = new Map<string, string>();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
if (key.startsWith(userProperties + "_")) {
const value = localStorage.getItem(key);
if (value) {
const userKey = key.substr((userProperties + "_").length);
result.set(userKey, JSON.parse(value));
}
}
}
}
return result;
} }
setUserProperty(name: string, value: string): void { setUserProperty(name: string, value: unknown): void {
localStorage.setItem(userProperties + "_" + name, value); localStorage.setItem(userProperties + "_" + name, JSON.stringify(value));
} }
} }

View File

@ -1223,19 +1223,6 @@ ${escapedMessage}
}; };
}); });
//TODO : move Player Properties related-code
iframeListener.registerAnswerer("setPlayerProperty", (event) => {
localUserStore.setUserProperty(event.propertyName, event.propertyValue as string);
});
iframeListener.registerAnswerer("getPlayerProperty", (event) => {
return {
propertyName: event,
propertyValue: localUserStore.getUserProperty(event),
};
});
//END TODO
iframeListener.registerAnswerer("getState", async () => { iframeListener.registerAnswerer("getState", async () => {
// The sharedVariablesManager is not instantiated before the connection is established. So we need to wait // The sharedVariablesManager is not instantiated before the connection is established. So we need to wait
// for the connection to send back the answer. // for the connection to send back the answer.
@ -1248,6 +1235,7 @@ ${escapedMessage}
roomId: this.roomUrl, roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [], tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables, variables: this.sharedVariablesManager.variables,
playerVariables: localUserStore.getAllUserProperties(),
userRoomToken: this.connection ? this.connection.userRoomToken : "", userRoomToken: this.connection ? this.connection.userRoomToken : "",
}; };
}); });
@ -1338,6 +1326,22 @@ ${escapedMessage}
}) })
); );
iframeListener.registerAnswerer("setVariable", (event, source) => {
switch (event.target) {
case "global": {
this.sharedVariablesManager.setVariable(event, source);
break;
}
case "player": {
localUserStore.setUserProperty(event.key, event.value);
break;
}
default: {
const _exhaustiveCheck: never = event.target;
}
}
});
iframeListener.registerAnswerer("removeActionMessage", (message) => { iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid); layoutManagerActionStore.removeAction(message.uuid);
}); });
@ -1480,6 +1484,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("openCoWebsite");
iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("getCoWebsites");
iframeListener.unregisterAnswerer("setPlayerOutline"); iframeListener.unregisterAnswerer("setPlayerOutline");
iframeListener.unregisterAnswerer("setVariable");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close(); this.embeddedWebsiteManager?.close();

View File

@ -3,6 +3,7 @@ import { iframeListener } from "../../Api/IframeListener";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent";
interface Variable { interface Variable {
defaultValue: unknown; defaultValue: unknown;
@ -48,11 +49,12 @@ export class SharedVariablesManager {
iframeListener.setVariable({ iframeListener.setVariable({
key: name, key: name,
value: value, value: value,
target: "global",
}); });
}); });
}
// When a variable is modified from an iFrame public setVariable(event: SetVariableEvent, source: MessageEventSource | null): void {
iframeListener.registerAnswerer("setVariable", (event, source) => {
const key = event.key; const key = event.key;
const object = this.variableObjects.get(key); const object = this.variableObjects.get(key);
@ -92,7 +94,6 @@ export class SharedVariablesManager {
// Dispatch to other iframes // Dispatch to other iframes
iframeListener.dispatchVariableToOtherIframes(key, event.value, source); iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
});
} }
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> { private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
@ -164,10 +165,6 @@ export class SharedVariablesManager {
return variable; return variable;
} }
public close(): void {
iframeListener.unregisterAnswerer("setVariable");
}
get variables(): Map<string, unknown> { get variables(): Map<string, unknown> {
return this._variables; return this._variables;
} }

View File

@ -14,25 +14,28 @@ import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui"; import ui from "./Api/iframe/ui";
import sound from "./Api/iframe/sound"; import sound from "./Api/iframe/sound";
import room, { setMapURL, setRoomId } from "./Api/iframe/room"; import room, { setMapURL, setRoomId } from "./Api/iframe/room";
import state, { initVariables } from "./Api/iframe/state"; import { createState } from "./Api/iframe/state";
import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player"; import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound"; import type { Sound } from "./Api/iframe/Sound/Sound";
import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution"; import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution";
const globalState = createState("global");
// Notify WorkAdventure that we are ready to receive data // Notify WorkAdventure that we are ready to receive data
const initPromise = queryWorkadventure({ const initPromise = queryWorkadventure({
type: "getState", type: "getState",
data: undefined, data: undefined,
}).then((state) => { }).then((gameState) => {
setPlayerName(state.nickname); setPlayerName(gameState.nickname);
setRoomId(state.roomId); setRoomId(gameState.roomId);
setMapURL(state.mapUrl); setMapURL(gameState.mapUrl);
setTags(state.tags); setTags(gameState.tags);
setUuid(state.uuid); setUuid(gameState.uuid);
initVariables(state.variables as Map<string, unknown>); globalState.initVariables(gameState.variables as Map<string, unknown>);
setUserRoomToken(state.userRoomToken); player.state.initVariables(gameState.playerVariables as Map<string, unknown>);
setUserRoomToken(gameState.userRoomToken);
}); });
const wa = { const wa = {
@ -43,7 +46,7 @@ const wa = {
sound, sound,
room, room,
player, player,
state, state: globalState,
onInit(): Promise<void> { onInit(): Promise<void> {
return initPromise; return initPromise;
@ -225,7 +228,5 @@ window.addEventListener(
callback?.callback(payloadData); callback?.callback(payloadData);
} }
} }
// ...
} }
); );