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 89cd292527
commit bf69b55e99
10 changed files with 153 additions and 159 deletions

View File

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

View File

@ -150,14 +150,6 @@ export const iframeQueryMapTypeGuards = {
query: isCreateEmbeddedWebsiteEvent, query: isCreateEmbeddedWebsiteEvent,
answer: tg.isUndefined, answer: tg.isUndefined,
}, },
getPlayerProperty: {
query: tg.isString,
answer: isPlayerPropertyEvent,
},
setPlayerProperty: {
query: isPlayerPropertyEvent,
answer: tg.isUndefined,
},
}; };
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;

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>();
@ -26,6 +26,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",
@ -68,20 +70,6 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
} }
return uuid; return uuid;
} }
getPlayerProperty(name: string): Promise<PlayerPropertyEvent> {
return queryWorkadventure({
type: "getPlayerProperty",
data: name,
});
}
setPlayerProperty(property: PlayerPropertyEvent) {
queryWorkadventure({
type: "setPlayerProperty",
data: property,
}).catch((e) => console.error(e));
}
} }
export default new WorkadventurePlayerCommands(); export default new WorkadventurePlayerCommands();

View File

@ -8,93 +8,101 @@ 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>();
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> { 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 = [ 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 } {
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { return new Proxy(new WorkadventureStateCommands(target), {
if (p in target) { get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
return Reflect.get(target, p, receiver); if (p in target) {
} return Reflect.get(target, p, receiver);
return target.loadVariable(p.toString()); }
}, 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. set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
// User must use WA.state.saveVariable to have error message. // Note: when using "set", there is no way to wait, so we ignore the return of the promise.
target.saveVariable(p.toString(), value); // User must use WA.state.saveVariable to have error message.
return true; target.saveVariable(p.toString(), value);
},
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
return true; return true;
} },
return target.hasVariable(p.toString()); has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
}, if (p in target) {
}) as WorkadventureStateCommands & { [key: string]: unknown }; return true;
}
export default proxyCommand; return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
}

View File

@ -204,12 +204,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

@ -1153,19 +1153,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.
@ -1178,6 +1165,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(),
}; };
}); });
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
@ -1267,6 +1255,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);
}); });
@ -1391,6 +1395,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("removeActionMessage"); iframeListener.unregisterAnswerer("removeActionMessage");
iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("openCoWebsite");
iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("getCoWebsites");
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, ITiledMapObjectLayer } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } 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,51 +49,51 @@ 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);
if (object === undefined) { if (object === undefined) {
const errMsg = const errMsg =
'A script is trying to modify variable "' + 'A script is trying to modify variable "' +
key + key +
'" but this variable is not defined in the map.' + '" but this variable is not defined in the map.' +
'There should be an object in the map whose name is "' + 'There should be an object in the map whose name is "' +
key + key +
'" and whose type is "variable"'; '" and whose type is "variable"';
console.error(errMsg); console.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) { if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
const errMsg = const errMsg =
'A script is trying to modify variable "' + 'A script is trying to modify variable "' +
key + key +
'" but this variable is only writable for users with tag "' + '" but this variable is only writable for users with tag "' +
object.writableBy + object.writableBy +
'".'; '".';
console.error(errMsg); console.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
// Let's stop any propagation of the value we set is the same as the existing value. // Let's stop any propagation of the value we set is the same as the existing value.
if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) { if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
return; return;
} }
this._variables.set(key, event.value); this._variables.set(key, event.value);
// Dispatch to the room connection. // Dispatch to the room connection.
this.roomConnection.emitSetVariableEvent(key, event.value); this.roomConnection.emitSetVariableEvent(key, event.value);
// 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,24 +14,27 @@ 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, setUuid } from "./Api/iframe/player"; import player, { setPlayerName, setTags, 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, sendToWorkadventure } 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>);
player.state.initVariables(gameState.playerVariables as Map<string, unknown>);
}); });
const wa = { const wa = {
@ -42,7 +45,7 @@ const wa = {
sound, sound,
room, room,
player, player,
state, state: globalState,
onInit(): Promise<void> { onInit(): Promise<void> {
return initPromise; return initPromise;
@ -224,7 +227,5 @@ window.addEventListener(
callback?.callback(payloadData); callback?.callback(payloadData);
} }
} }
// ...
} }
); );