diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index ca2a01cb..a90d9397 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -61,6 +61,10 @@ jobs: run: yarn run lint working-directory: "front" + - name: "Pretty" + run: yarn run pretty + working-directory: "front" + - name: "Jasmine" run: yarn test working-directory: "front" diff --git a/back/package.json b/back/package.json index 58399ad7..bb54d624 100644 --- a/back/package.json +++ b/back/package.json @@ -40,7 +40,7 @@ }, "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { - "@workadventure/tiled-map-type-guard": "^1.0.0", + "@workadventure/tiled-map-type-guard": "^1.0.2", "axios": "^0.21.1", "busboy": "^0.3.1", "circular-json": "^0.5.9", diff --git a/back/src/Services/VariablesManager.ts b/back/src/Services/VariablesManager.ts index e8aaef25..915c6c05 100644 --- a/back/src/Services/VariablesManager.ts +++ b/back/src/Services/VariablesManager.ts @@ -1,7 +1,12 @@ /** * Handles variables shared between the scripting API and the server. */ -import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist"; +import { + ITiledMap, + ITiledMapLayer, + ITiledMapObject, + ITiledMapObjectLayer, +} from "@workadventure/tiled-map-type-guard/dist"; import { User } from "_Model/User"; import { variablesRepository } from "./Repository/VariablesRepository"; import { redisClient } from "./RedisClient"; @@ -83,25 +88,33 @@ export class VariablesManager { private static findVariablesInMap(map: ITiledMap): Map { const objects = new Map(); for (const layer of map.layers) { - if (layer.type === "objectgroup") { - for (const object of (layer as ITiledMapObjectLayer).objects) { - if (object.type === "variable") { - if (object.template) { - console.warn( - 'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.' - ); - continue; - } - - // We store a copy of the object (to make it immutable) - objects.set(object.name, this.iTiledObjectToVariable(object)); - } - } - } + this.recursiveFindVariablesInLayer(layer, objects); } return objects; } + private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map): void { + if (layer.type === "objectgroup") { + for (const object of layer.objects) { + if (object.type === "variable") { + if (object.template) { + console.warn( + 'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.' + ); + continue; + } + + // We store a copy of the object (to make it immutable) + objects.set(object.name, this.iTiledObjectToVariable(object)); + } + } + } else if (layer.type === "group") { + for (const innerLayer of layer.layers) { + this.recursiveFindVariablesInLayer(innerLayer, objects); + } + } + } + private static iTiledObjectToVariable(object: ITiledMapObject): Variable { const variable: Variable = {}; diff --git a/back/yarn.lock b/back/yarn.lock index 2265f4da..64dcb9ce 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -194,10 +194,10 @@ semver "^7.3.2" tsutils "^3.17.1" -"@workadventure/tiled-map-type-guard@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba" - integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A== +"@workadventure/tiled-map-type-guard@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.2.tgz#4171550f6cd71be19791faef48360d65d698bcb0" + integrity sha512-RCtygGV5y9cb7QoyGMINBE9arM5pyXjkxvXgA5uXEv4GDbXKorhFim/rHgwbVR+eFnVF3rDgWbRnk3DIaHt+lQ== dependencies: generic-type-guard "^3.4.1" diff --git a/docs/maps/api-state.md b/docs/maps/api-state.md index 1cc4f7fb..a8ee5589 100644 --- a/docs/maps/api-state.md +++ b/docs/maps/api-state.md @@ -14,7 +14,7 @@ WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subsc WA.state.[any property]: unknown ``` -These methods and properties can be used to save, load and track changes in variables related to the current room. +These methods and properties can be used to save, load and track changes in [variables related to the current room](variables.md). Variables stored in `WA.state` can be any value that is serializable in JSON. @@ -63,44 +63,11 @@ that you get the expected type). For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data). Variables storage is subject to an authorization process. Read below to learn more. -### Declaring allowed keys +## Defining a variable -In order to declare allowed keys related to a room, you need to add **objects** in an "object layer" of the map. - -Each object will represent a variable. - -
-
- -
-
- -The name of the variable is the name of the object. -The object **type** MUST be **variable**. - -You can set a default value for the object in the `default` property. - -### Persisting variables state - -Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay -in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the -server restarts). - -{.alert.alert-info} -Do not use `persist` for highly dynamic values that have a short life spawn. - -### Managing access rights to variables - -With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string -representing a "tag". Anyone having this "tag" can read/write in the variable. - -{.alert.alert-warning} -`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags -is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure). - -Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable. -Trying to set a variable to a value that is not compatible with the schema will fail. +Out of the box, you cannot edit *any* variable. Variables MUST be declared in the map. +Check the [dedicated variables page](variables.md) to learn how to declare a variable in a map. ## Tracking variables changes diff --git a/docs/maps/variables.md b/docs/maps/variables.md new file mode 100644 index 00000000..17e803d9 --- /dev/null +++ b/docs/maps/variables.md @@ -0,0 +1,59 @@ +{.section-title.accent.text-primary} +# Variables + +Maps can contain **variables**. Variables are piece of information that store some data. In computer science, we like +to say variables are storing the "state" of the room. + +- Variables are shared amongst all players in a given room. When the value of a variable changes for one player, it changes + for everyone. +- Variables are **invisible**. There are plenty of ways they can act on the room, but by default, you don't see them. + +## Declaring a variable + +In order to declare allowed variables in a room, you need to add **objects** in an "object layer" of the map. + +Each object will represent a variable. + +
+
+ +
+
+ +The name of the variable is the name of the object. +The object **type** MUST be **variable**. + +You can set a default value for the object in the `default` property. + +## Persisting variables state + +Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay +in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the +server restarts). + +{.alert.alert-info} +Do not use `persist` for highly dynamic values that have a short life spawn. + +## Managing access rights to variables + +With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string +representing a "tag". Anyone having this "tag" can read/write in the variable. + +{.alert.alert-warning} +`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags +is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure). + +In a future release, the `jsonSchema` property will contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable. +Trying to set a variable to a value that is not compatible with the schema will fail. + +## Using variables + +There are plenty of ways to use variables in WorkAdventure: + +- Using the [scripting API](api-state.md), you can read, edit or track the content of variables. +- Using the [Action zones](https://workadventu.re/map-building-extra/generic-action-zones.md), you can set the value of a variable when someone is entering or leaving a zone +- By [binding variable values to properties in the map](https://workadventu.re/map-building-extra/variable-to-property-binding.md) +- By [using automatically generated configuration screens](https://workadventu.re/map-building-extra/automatic-configuration.md) to create forms to edit the value of variables + +In general, variables can be used by third party libraries that you can embed in your map to add extra features. +A good example of such a library is the ["Scripting API Extra" library](https://workadventu.re/map-building-extra/about.md) diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html new file mode 100644 index 00000000..e69de29b diff --git a/front/src/Api/Events/ButtonClickedEvent.ts b/front/src/Api/Events/ButtonClickedEvent.ts index de807037..26a8aceb 100644 --- a/front/src/Api/Events/ButtonClickedEvent.ts +++ b/front/src/Api/Events/ButtonClickedEvent.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isButtonClickedEvent = - new tg.IsInterface().withProperties({ +export const isButtonClickedEvent = new tg.IsInterface() + .withProperties({ popupId: tg.isNumber, buttonId: tg.isNumber, - }).get(); + }) + .get(); /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. */ diff --git a/front/src/Api/Events/ChatEvent.ts b/front/src/Api/Events/ChatEvent.ts index 5729a120..984859e8 100644 --- a/front/src/Api/Events/ChatEvent.ts +++ b/front/src/Api/Events/ChatEvent.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isChatEvent = - new tg.IsInterface().withProperties({ +export const isChatEvent = new tg.IsInterface() + .withProperties({ message: tg.isString, author: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. */ diff --git a/front/src/Api/Events/ClosePopupEvent.ts b/front/src/Api/Events/ClosePopupEvent.ts index 83b09c96..f604a404 100644 --- a/front/src/Api/Events/ClosePopupEvent.ts +++ b/front/src/Api/Events/ClosePopupEvent.ts @@ -1,9 +1,10 @@ import * as tg from "generic-type-guard"; -export const isClosePopupEvent = - new tg.IsInterface().withProperties({ +export const isClosePopupEvent = new tg.IsInterface() + .withProperties({ popupId: tg.isNumber, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/EnterLeaveEvent.ts b/front/src/Api/Events/EnterLeaveEvent.ts index 0c0cb4ff..ca68136e 100644 --- a/front/src/Api/Events/EnterLeaveEvent.ts +++ b/front/src/Api/Events/EnterLeaveEvent.ts @@ -1,9 +1,10 @@ import * as tg from "generic-type-guard"; -export const isEnterLeaveEvent = - new tg.IsInterface().withProperties({ +export const isEnterLeaveEvent = new tg.IsInterface() + .withProperties({ name: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. */ diff --git a/front/src/Api/Events/GoToPageEvent.ts b/front/src/Api/Events/GoToPageEvent.ts index cb258b03..d8d6467d 100644 --- a/front/src/Api/Events/GoToPageEvent.ts +++ b/front/src/Api/Events/GoToPageEvent.ts @@ -1,11 +1,10 @@ import * as tg from "generic-type-guard"; - - -export const isGoToPageEvent = - new tg.IsInterface().withProperties({ +export const isGoToPageEvent = new tg.IsInterface() + .withProperties({ url: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/LoadSoundEvent.ts b/front/src/Api/Events/LoadSoundEvent.ts index 19b4b8e1..f48f202f 100644 --- a/front/src/Api/Events/LoadSoundEvent.ts +++ b/front/src/Api/Events/LoadSoundEvent.ts @@ -1,9 +1,10 @@ import * as tg from "generic-type-guard"; -export const isLoadSoundEvent = - new tg.IsInterface().withProperties({ +export const isLoadSoundEvent = new tg.IsInterface() + .withProperties({ url: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/OpenPopupEvent.ts b/front/src/Api/Events/OpenPopupEvent.ts index 094ba555..c1070bbe 100644 --- a/front/src/Api/Events/OpenPopupEvent.ts +++ b/front/src/Api/Events/OpenPopupEvent.ts @@ -1,18 +1,20 @@ import * as tg from "generic-type-guard"; -const isButtonDescriptor = - new tg.IsInterface().withProperties({ +const isButtonDescriptor = new tg.IsInterface() + .withProperties({ label: tg.isString, - className: tg.isOptional(tg.isString) - }).get(); + className: tg.isOptional(tg.isString), + }) + .get(); -export const isOpenPopupEvent = - new tg.IsInterface().withProperties({ +export const isOpenPopupEvent = new tg.IsInterface() + .withProperties({ popupId: tg.isNumber, targetObject: tg.isString, message: tg.isString, - buttons: tg.isArray(isButtonDescriptor) - }).get(); + buttons: tg.isArray(isButtonDescriptor), + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/OpenTabEvent.ts b/front/src/Api/Events/OpenTabEvent.ts index e510f8b6..6fe6ec21 100644 --- a/front/src/Api/Events/OpenTabEvent.ts +++ b/front/src/Api/Events/OpenTabEvent.ts @@ -1,11 +1,10 @@ import * as tg from "generic-type-guard"; - - -export const isOpenTabEvent = - new tg.IsInterface().withProperties({ +export const isOpenTabEvent = new tg.IsInterface() + .withProperties({ url: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/PlaySoundEvent.ts b/front/src/Api/Events/PlaySoundEvent.ts index 33ca1ff4..6fe56746 100644 --- a/front/src/Api/Events/PlaySoundEvent.ts +++ b/front/src/Api/Events/PlaySoundEvent.ts @@ -1,22 +1,23 @@ import * as tg from "generic-type-guard"; - -const isSoundConfig = - new tg.IsInterface().withProperties({ +const isSoundConfig = new tg.IsInterface() + .withProperties({ volume: tg.isOptional(tg.isNumber), loop: tg.isOptional(tg.isBoolean), mute: tg.isOptional(tg.isBoolean), rate: tg.isOptional(tg.isNumber), detune: tg.isOptional(tg.isNumber), seek: tg.isOptional(tg.isNumber), - delay: tg.isOptional(tg.isNumber) - }).get(); + delay: tg.isOptional(tg.isNumber), + }) + .get(); -export const isPlaySoundEvent = - new tg.IsInterface().withProperties({ +export const isPlaySoundEvent = new tg.IsInterface() + .withProperties({ url: tg.isString, - config : tg.isOptional(isSoundConfig), - }).get(); + config: tg.isOptional(isSoundConfig), + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/StopSoundEvent.ts b/front/src/Api/Events/StopSoundEvent.ts index 6d12516d..cdfe43ca 100644 --- a/front/src/Api/Events/StopSoundEvent.ts +++ b/front/src/Api/Events/StopSoundEvent.ts @@ -1,9 +1,10 @@ import * as tg from "generic-type-guard"; -export const isStopSoundEvent = - new tg.IsInterface().withProperties({ +export const isStopSoundEvent = new tg.IsInterface() + .withProperties({ url: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/UserInputChatEvent.ts b/front/src/Api/Events/UserInputChatEvent.ts index de21ff6e..9de41327 100644 --- a/front/src/Api/Events/UserInputChatEvent.ts +++ b/front/src/Api/Events/UserInputChatEvent.ts @@ -1,9 +1,10 @@ import * as tg from "generic-type-guard"; -export const isUserInputChatEvent = - new tg.IsInterface().withProperties({ +export const isUserInputChatEvent = new tg.IsInterface() + .withProperties({ message: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the game to the iFrame when a user types a message in the chat. */ diff --git a/front/src/Api/iframe/Ui/ButtonDescriptor.ts b/front/src/Api/iframe/Ui/ButtonDescriptor.ts index 119daf5c..9cf1688a 100644 --- a/front/src/Api/iframe/Ui/ButtonDescriptor.ts +++ b/front/src/Api/iframe/Ui/ButtonDescriptor.ts @@ -1,4 +1,4 @@ -import type {Popup} from "./Popup"; +import type { Popup } from "./Popup"; export type ButtonClickedCallback = (popup: Popup) => void; @@ -6,13 +6,13 @@ export interface ButtonDescriptor { /** * The label of the button */ - label: string, + label: string; /** * The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled" */ - className?: "normal" | "primary" | "success" | "warning" | "error" | "disabled", + className?: "normal" | "primary" | "success" | "warning" | "error" | "disabled"; /** * Callback called if the button is pressed */ - callback: ButtonClickedCallback, + callback: ButtonClickedCallback; } diff --git a/front/src/Api/iframe/Ui/Popup.ts b/front/src/Api/iframe/Ui/Popup.ts index 37dea922..085fdc2c 100644 --- a/front/src/Api/iframe/Ui/Popup.ts +++ b/front/src/Api/iframe/Ui/Popup.ts @@ -1,19 +1,18 @@ -import {sendToWorkadventure} from "../IframeApiContribution"; -import type {ClosePopupEvent} from "../../Events/ClosePopupEvent"; +import { sendToWorkadventure } from "../IframeApiContribution"; +import type { ClosePopupEvent } from "../../Events/ClosePopupEvent"; export class Popup { - constructor(private id: number) { - } + constructor(private id: number) {} /** * Closes the popup */ public close(): void { sendToWorkadventure({ - 'type': 'closePopup', - 'data': { - 'popupId': this.id, - } as ClosePopupEvent + type: "closePopup", + data: { + popupId: this.id, + } as ClosePopupEvent, }); } } diff --git a/front/src/Api/iframe/registeredCallbacks.ts b/front/src/Api/iframe/registeredCallbacks.ts index 5d6f784d..3b6ee6c7 100644 --- a/front/src/Api/iframe/registeredCallbacks.ts +++ b/front/src/Api/iframe/registeredCallbacks.ts @@ -1,16 +1,18 @@ -import type {IframeResponseEventMap} from "../../Api/Events/IframeEvent"; -import type {IframeCallback} from "../../Api/iframe/IframeApiContribution"; -import type {IframeCallbackContribution} from "../../Api/iframe/IframeApiContribution"; +import type { IframeResponseEventMap } from "../../Api/Events/IframeEvent"; +import type { IframeCallback } from "../../Api/iframe/IframeApiContribution"; +import type { IframeCallbackContribution } from "../../Api/iframe/IframeApiContribution"; -export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback } = {} +export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback } = {}; -export function apiCallback(callbackData: IframeCallbackContribution): IframeCallbackContribution { +export function apiCallback( + callbackData: IframeCallbackContribution +): IframeCallbackContribution { const iframeCallback = { typeChecker: callbackData.typeChecker, - callback: callbackData.callback + callback: callbackData.callback, } as IframeCallback; const newCallback = { [callbackData.type]: iframeCallback }; - Object.assign(registeredCallbacks, newCallback) + Object.assign(registeredCallbacks, newCallback); return callbackData as unknown as IframeCallbackContribution; } diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index a78b4bde..a448d56c 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -1,6 +1,4 @@
@@ -55,22 +62,22 @@ Switch to presentation mode {/if}
-
- {#if $requestedScreenSharingState} +
+ {#if $requestedScreenSharingState && !isSilent} Start screen sharing {:else} Stop screen sharing {/if}
-
- {#if $requestedCameraState} +
+ {#if $requestedCameraState && !isSilent} Turn on webcam {:else} Turn off webcam {/if}
-
- {#if $requestedMicrophoneState} +
+ {#if $requestedMicrophoneState && !isSilent} Turn on microphone {:else} Turn off microphone diff --git a/front/src/Components/Menu/ProfileSubMenu.svelte b/front/src/Components/Menu/ProfileSubMenu.svelte index 516baf6b..39214b4f 100644 --- a/front/src/Components/Menu/ProfileSubMenu.svelte +++ b/front/src/Components/Menu/ProfileSubMenu.svelte @@ -1,13 +1,17 @@
+ {#if $userIsConnected} +
+ {#if PROFILE_URL != undefined} + + {/if} +
+
+ +
+ {:else} +
+ Sing in +
+ {/if}
-
-
+
- +