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

This commit is contained in:
David Négrier 2021-09-16 16:46:27 +02:00
commit 4728c3a59a
209 changed files with 5546 additions and 3739 deletions

View file

@ -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"

View file

@ -1,5 +1,14 @@
## Version develop
### Updates
- New scripting API features :
- Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu.
- New `jitsiWidth` parameter to set the width of Jitsi
- Refactored the way videos are displayed to better cope for vertical videos (on mobile)
- Fixing reconnection issues after 5 minutes of an inactive tab on Google Chrome
## Version 1.4.14
### Updates
- New scripting API features :
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.

View file

@ -35,6 +35,9 @@ Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
127.0.0.1 workadventure.localhost
```
Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``.
Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.testing and accepting them.
### MacOS developers, your environment with Vagrant
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).

View file

@ -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",
@ -54,7 +54,6 @@
"prom-client": "^12.0.0",
"query-string": "^6.13.3",
"redis": "^3.1.2",
"systeminformation": "^4.31.1",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
"uuidv4": "^6.0.7"
},

View file

@ -9,7 +9,6 @@ const JITSI_ISS = process.env.JITSI_ISS || "";
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080;
const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const REDIS_HOST = process.env.REDIS_HOST || undefined;

View file

@ -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<string, Variable> {
const objects = new Map<string, Variable>();
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<string, Variable>): 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 = {};

View file

@ -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"
@ -554,7 +554,7 @@ chokidar@^3.4.0:
optionalDependencies:
fsevents "~2.1.2"
chownr@^1.1.1:
chownr@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
@ -1159,7 +1159,7 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
fs-minipass@^1.2.5:
fs-minipass@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
@ -1969,7 +1969,7 @@ minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
minipass@^2.6.0, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
@ -1977,7 +1977,7 @@ minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minizlib@^1.2.1:
minizlib@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
@ -1992,7 +1992,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3:
mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@ -2290,9 +2290,9 @@ path-key@^3.0.0, path-key@^3.1.0:
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-type@^1.0.0:
version "1.1.0"
@ -2578,7 +2578,7 @@ rxjs@^6.6.7:
dependencies:
tslib "^1.9.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -2962,11 +2962,6 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
systeminformation@^4.31.1:
version "4.31.1"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.31.1.tgz#2e02c26987494d4b6a4d2d83138724593bc98d50"
integrity sha512-dVCDWNMN8ncMZo5vbMCA5dpAdMgzafK2ucuJy5LFmGtp1cG6farnPg8QNvoOSky9SkFoEX1Aw0XhcOFV6TnLYA==
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@ -2978,17 +2973,17 @@ table@^5.2.3:
string-width "^3.0.0"
tar@^4.4.2:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
version "4.4.19"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==
dependencies:
chownr "^1.1.1"
fs-minipass "^1.2.5"
minipass "^2.8.6"
minizlib "^1.2.1"
mkdirp "^0.5.0"
safe-buffer "^5.1.2"
yallist "^3.0.3"
chownr "^1.1.4"
fs-minipass "^1.2.7"
minipass "^2.9.0"
minizlib "^1.3.3"
mkdirp "^0.5.5"
safe-buffer "^5.2.1"
yallist "^3.1.1"
tdigest@^0.1.1:
version "0.1.1"
@ -3282,7 +3277,7 @@ y18n@^3.2.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
yallist@^3.0.0, yallist@^3.0.3:
yallist@^3.0.0, yallist@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==

View file

@ -429,9 +429,9 @@
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"path-type": {
"version": "1.1.0",

View file

@ -315,8 +315,8 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
path-type@^1.0.0:
version "1.1.0"

View file

@ -75,6 +75,9 @@
"UPLOADER_URL": "//uploader-"+url,
"ADMIN_URL": "//"+url,
"JITSI_URL": env.JITSI_URL,
#POSTHOG
"POSTHOG_API_KEY": if namespace == "master" then env.POSTHOG_API_KEY else "",
"POSTHOG_URL": if namespace == "master" then env.POSTHOG_URL else "",
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
@ -101,6 +104,7 @@
},
"redis": {
"image": "redis:6",
"ports": [6379]
}
},
"config": {

View file

@ -18,3 +18,4 @@ The list of functions below is **deprecated**. You should not use those but. use
- Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`.
- Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`.
- Method `WA.onLeaveZone` is deprecated. It has been renamed to `WA.room.onLeaveZone`.
- Method `WA.ui.registerMenuCommand` parameter `callback` is deprecated. Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions)`.

View file

@ -9,11 +9,12 @@ Moreover, `WA.state` functions can be used to persist this state across reloads.
```
WA.state.saveVariable(key : string, data : unknown): void
WA.state.loadVariable(key : string) : unknown
WA.state.hasVariable(key : string) : boolean
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
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.
@ -62,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.
<div class="row">
<div class="col">
<img src="images/object_variable.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
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

View file

@ -68,25 +68,53 @@ WA.room.onLeaveZone('myZone', () => {
### Add custom menu
```typescript
WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void
```
Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`.
WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu
```
Add a custom menu item containing the text `commandDescriptor` in the navbar of the menu.
`options` attribute accepts an object with three properties :
- `callback : (commandDescriptor: string) => void` : A click on the custom menu will trigger the `callback`.
- `iframe: string` : A click on the custom menu will open the `iframe` inside the menu.
- `allowApi?: boolean` : Allow the iframe of the custom menu to use the Scripting API.
Important : `options` accepts only `callback` or `iframe` not both.
Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script.
Example:
<div class="row">
<div class="col">
<img src="images/custom-menu-navbar.png" class="figure-img img-fluid rounded" alt="" />
</div>
<div class="col">
<img src="images/custom-menu-iframe.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
Example:
```javascript
const menu = WA.ui.registerMenuCommand('menu test',
{
callback: () => {
WA.chat.sendChatMessage('test');
}
})
WA.ui.registerMenuCommand("test", () => {
WA.chat.sendChatMessage("test clicked", "menu cmd")
})
// Some time later, if you want to remove the menu:
menu.remove();
```
<div class="col">
<img src="images/menu-command.png" class="figure-img img-fluid rounded" alt="" />
</div>
Please note that `registerMenuCommand` returns an object of the `Menu` class.
The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called.
```javascript
class Menu {
/**
* Remove the menu
*/
remove() {};
}
```

67
docs/maps/entry-exit.md Normal file
View file

@ -0,0 +1,67 @@
{.section-title.accent.text-primary}
# Entries and exits
https://www.youtube.com/watch?v=MuhVgu8H7U0
## Defining a default entry point
In order to define a default start position, you MUST create a layer named "`start`" on your map. This layer MUST contain at least one tile. The players will start on the tile of this layer. If the layer contains many tiles, the players will start randomly on one of those tiles.
![Start layer screenshot](images/start_layer.png)
In the screenshot above, the start layer is made of the 2 white tiles. These tiles are not visible to the end user because they are hidden below the "bottom" layer that displays the floor of the map.
{.alert.alert-info}
**Pro tip**: if you expect many people to connect to your map at the same time (for instance, if you are organizing a big event), consider making a large start zone. This way, users will not all appear at the same position and will not pop randomly in a chat with someone connecting at the same moment.
## Defining exits
In order to place an exit on your scene that leads to another scene:
* You must create a specific layer. When a character reaches ANY tile of that layer, it will exit the scene.
* In layer properties, you MUST add "`exitUrl`" property. It represents the URL of the next scene. You can put relative or absolute URLs.
* If you want to have multiple exits, you can create many layers. Each layer has a different key `exitUrl` and has tiles that represent exits to another scene.
![](images/exit_layer_map.png)
{.alert.alert-warning}
**Note:** in older releases of WorkAdventure, you could link to a map file directly using properties `exitSceneUrl` and `exitInstance`. Those properties are now **deprecated**. Use "`exitUrl`" instead.
## Understanding map URLs in WorkAdventure
There are 2 kinds of URLs in WorkAdventure:
* Public URLs are in the form `https://play.workadventu.re/_/[instance]/[server]/[path to map]`
* Private URLs (used in paid accounts) are in the form `https://play.workadventu.re/@/[organization]/[world]/[map]`
Assuming your JSON map is hosted at "`https://example.com/my/map.json`", then you can browse your map at "`https://play.workadventu.re/_/global/example.com/my/map.json`". Here, "global" is a name of an "instance" of your map. You can put anything instead of "global" here. People on the same instance of the map can see each others. If 2 users use 2 different instances, they are on the same map, but in 2 parallel universes. They cannot see each other.
## Defining several entry points
Often your map will have several exits, and therefore, several entry points. For instance, if there is an exit by a door that leads to the garden map, when you come back from the garden you expect to come back by the same door. Therefore, a map can have several entry points. Those entry points are "named" (they have a name).
In order to create a named entry point:
You can create a new layer for your entry point or use an existing layer with named tiles.
* If you don't use the layer named "`start`", you MUST add a boolean "`startLayer`" property to the layer properties. It MUST be set to true.
* If you use this method, when a character enters the map by this entry point, it will enter randomly on ANY tile of that layer. The name of the entry point is the name of that layer.
![](images/layer-entry-point.png)
You can also use the tiles properties to create entry point.
* To do that, you will need to have a layer named "`start`" or with the "`startLayer`" property. Then you MUST add a string "`start`" property to a tile than you use in that layer. The name of the entry point is the value that property.
* If you use this method, when a character enters the map by this entry point, it will enter on ANY tile of the same kind in that layer.
![](images/tile-entry-point.png)
Notes :
* Two tiles with a string "start" property with different value can be in the same layer of entries.
* A tile with a string "start" property that is not in a layer of entries won't usable as an entry point.
How to use entry point :
* To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`".
* You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)

25
docs/maps/hosting.md Normal file
View file

@ -0,0 +1,25 @@
{.section-title.accent.text-primary}
# Hosting your map
The [Getting Started](.) page proposes to use a "starter kit" that is relying on GitHub pages for hosting the map. This is a fairly good solution as GitHub pages offers a free and performant hosting.
But using GitHub pages is not necessary. You can host your maps on any webserver.
{.alert.alert-warning}
If you decide to host your maps on your own webserver, you must **configure CORS headers** in your browser to allow access from WorkAdventure.
## Configuring CORS headers
CORS headers ([Cross Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)) are useful when a website want to make some resources accessible to another website. This is exactly what we want to do. We want the map you are designing to be accessible from the WorkAdventure domain (`play.workadventu.re`).
### Enabling CORS for Apache
In order to enable CORS in your Apache configuration, you will need to ensure the `headers` module is enabled.
In your Apache configuration file, simply add the following line inside either the `<Directory>`, `<Location>`, `<Files>` or `<VirtualHost>` sections, or within a `.htaccess` file.
Header set Access-Control-Allow-Origin "*"
### Enabling CORS on another webserver
Check out [enable-cors.org](https://enable-cors.org/server.html) which has detailed instructions on how to enable CORS on many different webservers.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

128
docs/maps/index.md Normal file
View file

@ -0,0 +1,128 @@
{.section-title.accent.text-primary}
# Create your map
## Tools you will need
In order to build your own map for WorkAdventure, you need:
* the [Tiled editor](https://www.mapeditor.org/) software
* "tiles" (i.e. images) to create your map
* a web-server to serve your map
WorkAdventure comes with a "map starter kit" that we recommend using to start designing your map quickly. It contains **a good default tileset** for building an office and it proposes to **use Github static pages as a web-server** which is both free and performant. It also comes with a local webserver for testing purpose and with Typescript support (if you are looking to use the [map scripting API]({{url('/map-building/scripting')}}).
{.alert.alert-info}
If you are looking to host your maps on your own webserver, be sure to read the [Self-hosting your map](hosting.md) guide.
[](https://www.youtube.com/watch?v=lu1IZgBJJD4)
## Getting started
Start by [creating a GitHub account](https://github.com/join) if you don't already have one.
Then, go to the [Github map starter kit repository page](https://github.com/thecodingmachine/workadventure-map-starter-kit) and click the **"Use this template"** button.
<figure class="figure my-5">
<img src="images/use_this_template.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">The "Use this template" button</figcaption>
</figure>
You will be prompted to enter a repository name for your map.
<figure class="figure my-5">
<img src="images/create_repo.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">The "create a new repository" page</figcaption>
</figure>
**Make sure to check the "Include all branches" checkbox, otherwise the Github Pages deployment process will not be setup automatically.**
If you miss that step, don't worry, you can always fix that by clicking on the **Settings tab** of your repository and scroll down to the **GitHub Pages** section. Then select the **gh-pages** branch. It might already be selected, but please be sure to click on it nonetheless (otherwise GitHub will not enable GitHub pages).
{.alert.alert-info}
If you only see a "master" branch and if the **gh-pages** branch does not appear here, simply wait a few minutes and refresh your page. When you created the project, Github Actions triggered a job that is in charge of creating the **gh-pages** branch. Maybe the job is not finished yet.
<figure class="figure my-5">
<img src="images/github_pages.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">The GitHub pages configuration section</figcaption>
</figure>
Wait a few minutes... Github will deploy a new website with the content of the repository. The address of the website is visible in the "GitHub Pages" section.
<figure class="figure my-5">
<img src="images/website_address.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">Your website is ready!</figcaption>
</figure>
Click on the link. You should be redirected directly to WorkAdventure, on your map!
## Customizing your map
Your map is now up and online, but this is still the demo map from the starter kit. You need to customize it.
### Cloning the map
Start by cloning the map. If you are used to Git and GitHub, simply clone the map to your computer using your preferred tool and [jump to the next chapter](#loading-the-map-in-tiled).
If you are new to Git, cloning the map means downloading the map to your computer. To do this, you will need Git, or a Git compatible tool. Our advice is to use [GitHub Desktop](https://desktop.github.com/). We recommend you take some time mastering the notion of pull / commit / push as this will make uploading your maps really easier.
As an (easier) alternative, you can simply use the "Export" button to download the code of the map in a big Zip file. When you want to upload your work again, you will simply drag'n'drop your files in the GitHub website.
### Loading the map in Tiled
The sample map is in the file `map.json`. You can load this file in [Tiled](https://www.mapeditor.org/).
Now, it's up to you to edit the map and write your own map.
Some resources regarding Tiled:
* [Tiled documentation](https://doc.mapeditor.org/en/stable/manual/introduction/)
* [Tiled video tutorials](https://www.gamefromscratch.com/post/2015/10/14/Tiled-Map-Editor-Tutorial-Series.aspx)
### Testing your map locally
In order to test your map, you need a webserver to host your map. The "map starter kit" comes with a local webserver that you can use to test your map.
In order to start the webserver, you will need [Node.JS](https://nodejs.org/en/). When it is downloaded, open your command line and from the directory of the map, run this command:
$ npm install
This will install the local webserver.
$ npm run start
This command will start the webserver and open the welcome page. You should see a page looking like this:
<figure class="figure my-5">
<img src="images/start_kit_start_screen.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">The welcome page of the "map start kit"</figcaption>
</figure>
From here, you simply need to click the "Test this map" button to test your map in WorkAdventure.
{.alert.alert-warning}
The local web server can only be used to test your map locally. In particular, the link will only work on your computer. You cannot share it with other people.
### Pushing the map
When your changes are ready, you need to "commit" and "push" (i.e. "upload") the changes back to GitHub. Just wait a few minutes, and your map will be propagated automatically to the GitHub pages web-server.
## Testing your map
To test your map, you need to find its URL. There are 2 kinds of URLs in WorkAdventure:
* Test URLs are in the form `https://play.workadventu.re/_/[instance]/[server]/[path to map]`
* Registered URLs are in the form `https://play.workadventu.re/@/[organization]/[world]/[map]`
Assuming your JSON map is hosted at "`https://myuser.github.io/myrepo/map.json`", then you can browse your map at "`https://play.workadventu.re/_/global/myuser.github.io/myrepo/map.json`". Here, "global" is a name of an "instance" of your map. You can put anything instead of "global" here. People on the same instance of the map can see each others. If 2 users use 2 different instances, they are on the same map, but in 2 parallel universes. They cannot see each other.
This will connect you to a "public" instance. Anyone can come and connect to a public instance. If you want to manage invitations, or to perform some moderation, you will need to create a "private" instance. Private instances are available in "pro" accounts.
<div class="card">
<div class="card-body">
<h3 id="need-some-help">Need some help?</h3>
<p>WorkAdventure is a constantly evolving project and there is plenty of room for improvement regarding map editing.</p>
<p>If you are facing any troubles, do not hesitate to open an "issue" in the
<a href="https://github.com/thecodingmachine/workadventure/issues" target="_blank">GitHub WorkAdventure account</a>.
</p>
</div>
</div>

View file

@ -0,0 +1,82 @@
{.section-title.accent.text-primary}
# Meeting rooms
https://www.youtube.com/watch?v=cN9VMWHN0eo
## Opening a Jitsi meet when walking on the map
On your map, you can define special zones (meeting rooms) that will trigger the opening of a Jitsi meet. When a player will pass over these zones, a Jitsi meet will open (as an iframe on the right side of the screen)
In order to create Jitsi meet zones:
* You must create a specific layer.
* In layer properties, you MUST add a "`jitsiRoom`" property (of type "`string`"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be "slugified" and prepended with the name of the instance of the map (so that different instances of the map have different rooms)
* You may also use "jitsiWidth" property (of type "number" between 0 and 100) to control the width of the iframe containing the meeting room.
## Triggering of the "Jitsi meet" action
By default, Jitsi meet will open when a user enters the zone defined on the map.
It is however possible to trigger Jitsi only on user action. You can do this with the `jitsiTrigger` property.
If you set `jitsiTrigger: onaction`, when the user walks on the layer, an alert message will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/click_space_jitsi.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">Jitsi meet will only open if the user clicks Space</figcaption>
</figure>
If you set `jitsiTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to enter in jitsi meet room'.
## Customizing your "Jitsi meet"
Your Jitsi meet experience can be customized using Jitsi specific config options. The `jitsiConfig` and `jitsiInterfaceConfig` properties can be used on the Jitsi layer to change the way Jitsi looks and behaves. Those 2 properties are accepting a JSON string.
For instance, use `jitsiConfig: { "startWithAudioMuted": true }` to automatically mute the microphone when someone enters a room. Or use `jitsiInterfaceConfig: { "DEFAULT_BACKGROUND": "#77ee77" }` to change the background color of Jitsi.
The `jitsiConfig` property will override the Jitsi [config.js](https://github.com/jitsi/jitsi-meet/blob/master/config.js) file
The `jitsiInterfaceConfig` property will override the Jitsi [interface_config.js](https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js) file
<div class="alert alert-warning">If your customizations are not working:
<ul>
<li>First, check that the JSON you are entering is valid. Take a look at the console in your browser. If the JSON string is invalid, you should see a warning.</li>
<li>Then, check that the JSON you are using is matching the version of Jitsi used.</li>
</ul>
</div>
## Granting moderator controls in Jitsi
{.alert.alert-info}
Moderator controls are linked to member tags. You need a pro account to edit member tags.
You can grant moderator rights to some of your members. Jitsi moderators can:
* Publish a Jitsi meeting on Youtube Live (you will need a Youtube Live account)
* Record a meeting to Dropbox (you will need a Dropbox account)
* Mute someone
* Mute everybody expect one speaker
* Kick users out of the meeting
In order to grant moderator rights to a given user, you can add a `jitsiRoomAdminTag` property to your Jitsi layer. For instance, if you write a property:
jitsiRoomAdminTag: speaker
then, any of your member with the `speaker` tag will be automatically granted moderator rights over this Jitsi instance.
You can read more about [managing member tags in the admin documentation](/admin-guide/manage-members).
## Using another Jitsi server
WorkAdventure usually comes with a default Jitsi meet installation. If you are using the online version at `workadventu.re`, we are handling a Jitsi meet cluster for you. If you are running the self-hosted version of WorkAdventure, the administrator probably set up a Jitsi meet instance too.
You have the possibility, in your map, to override the Jitsi meet instance that will be used by default. This can be useful for regulatory reasons. Maybe your company wants to keep control on the video streams and therefore, wants to self-host a Jitsi instance? Or maybe you want to use a very special configuration or very special version of Jitsi?
Use the `jitsiUrl` property to in the Jitsi layer to specify the Jitsi instance that should be used. Beware, `jitsiUrl` takes in parameter a **domain name**, without the protocol. So you should use:
`jitsiUrl: meet.jit.si`
and not
`jitsiUrl: https://meet.jit.si`
{.alert.alert-info}
When you use `jitsiUrl`, the targeted Jitsi instance must be public. You cannot use moderation features or the JWT
tokens authentication with maps configured using the `jitsiUrl` property.

133
docs/maps/menu.php Normal file
View file

@ -0,0 +1,133 @@
<?php
$extraMenu = require __DIR__.'/../../scripting_api_extra_doc/menu.php';
$extraUtilsMenu = require __DIR__.'/../../scripting_api_extra_doc/menu_functions.php';
return [
[
'title' => 'Getting started',
'url' => '/map-building',
'markdown' => 'maps.index'
],
[
'title' => 'WorkAdventure maps',
'url' => '/map-building/wa-maps',
'markdown' => 'maps.wa-maps'
],
[
'title' => 'Entries and exits',
'url' => '/map-building/entry-exit.md',
'markdown' => 'maps.entry-exit'
],
[
'title' => 'Opening a website',
'url' => '/map-building/opening-a-website.md',
'markdown' => 'maps.opening-a-website'
],
[
'title' => 'Meeting rooms',
'url' => '/map-building/meeting-rooms.md',
'markdown' => 'maps.meeting-rooms'
],
[
'title' => 'Special zones',
'url' => '/map-building/special-zones.md',
'markdown' => 'maps.special-zones'
],
[
'title' => 'Animations',
'url' => '/map-building/animations.md',
'markdown' => 'maps.animations'
],
[
'title' => 'Integrated websites',
'url' => '/map-building/website-in-map.md',
'markdown' => 'maps.website-in-map'
],
[
'title' => 'Variables',
'url' => '/map-building/variables.md',
'markdown' => 'maps.variables'
],
[
'title' => 'Self-hosting your map',
'url' => '/map-building/hosting.md',
'markdown' => 'maps.hosting'
],
$extraMenu,
[
'title' => 'Scripting maps',
'url' => '/map-building/scripting',
'markdown' => 'maps.scripting',
'children' => [
[
'title' => 'Using Typescript',
'url' => '/map-building/using-typescript.md',
'markdown' => 'maps.using-typescript'
],
[
'title' => 'API Reference',
'url' => '/map-building/api-reference',
'markdown' => 'maps.api-reference',
'collapse' => true,
'children' => [
[
'title' => 'Initialization',
'url' => '/map-building/api-start.md',
'markdown' => 'maps.api-start',
],
[
'title' => 'Navigation',
'url' => '/map-building/api-nav.md',
'markdown' => 'maps.api-nav',
],
[
'title' => 'Chat',
'url' => '/map-building/api-chat.md',
'markdown' => 'maps.api-chat',
],
[
'title' => 'Room',
'url' => '/map-building/api-room.md',
'markdown' => 'maps.api-room',
],
[
'title' => 'State',
'url' => '/map-building/api-state.md',
'markdown' => 'maps.api-state',
],
[
'title' => 'Player',
'url' => '/map-building/api-player.md',
'markdown' => 'maps.api-player',
],
[
'title' => 'UI',
'url' => '/map-building/api-ui.md',
'markdown' => 'maps.api-ui',
],
[
'title' => 'Sound',
'url' => '/map-building/api-sound.md',
'markdown' => 'maps.api-sound',
],
[
'title' => 'Controls',
'url' => '/map-building/api-controls.md',
'markdown' => 'maps.api-controls',
],
[
'title' => 'Deprecated',
'url' => '/map-building/api-deprecated.md',
'markdown' => 'maps.api-deprecated',
],
]
],
$extraUtilsMenu
]
],
[
'title' => 'Troubleshooting',
'url' => '/map-building/troubleshooting',
'view' => 'content.map.troubleshooting'
],
];

View file

@ -0,0 +1,66 @@
{.section-title.accent.text-primary}
# Opening a website when walking on the map
https://www.youtube.com/watch?v=Me8cu5lLN3A
## The openWebsite property
On your map, you can define special zones. When a player will pass over these zones, a website will open (as an iframe
on the right side of the screen)
In order to create a zone that opens websites:
* You must create a specific layer.
* In layer properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://")
* You may also use "`openWebsiteWidth`" property (of type "`int`" or "`float`" between 0 and 100) to control the width of the iframe.
* You may also use "`openTab`" property (of type "`string`") to open in a new tab instead.
{.alert.alert-warning}
A website can explicitly forbid another website from loading it in an iFrame using
the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options).
## Integrating a Youtube video
A common use case is to use `openWebsite` to open a Youtube video.
The global Youtube page cannot be embedded into an iFrame (it has the `X-Frame-Options` HTTP header).
To embed a Youtube video, be sure to **use the "embed" link**. You can get this link be clicking "Share > Embed" in Youtube.
<figure class="figure">
<img src="images/youtube.jpg" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">Find the URL of your video in the "embed Video" HTML snippet on Youtube</figcaption>
</figure>
<figure class="figure">
<img src="images/open_website.jpg" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">Put this URL in the "openWebsite" property</figcaption>
</figure>
### Triggering of the "open website" action
By default, the iFrame will open when a user enters the zone defined on the map.
It is however possible to trigger the iFrame only on user action. You can do this with the `openWebsiteTrigger` property.
If you set `openWebsiteTrigger: onaction`, when the user walks on the layer, an alert message will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/click_space_open_website.jpg" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The iFrame will only open if the user clicks Space</figcaption>
</figure>
If you set `openWebsiteTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to open the web site'.
### Setting the iFrame "allow" attribute
By default, iFrames have limited rights in browsers. For instance, they cannot put their content in fullscreen, they cannot start your webcam, etc...
If you want to grant additional access rights to your iFrame, you should use the `openWebsitePolicy` property. The value of this property will be directly used for the [`allow` atttribute of your iFrame](https://developer.mozilla.org/en-US/docs/Web/HTTP/Feature_Policy/Using_Feature_Policy#the_iframe_allow_attribute).
For instance, if you want an iFrame to be able to go in fullscreen, you will use the property `openWebsitePolicy: fullscreen`
<figure class="figure">
<img src="images/open_website_policy.jpg" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The generated iFrame will have the allow attribute set to: <code>&lt;iframe allow="fullscreen"&gt;</code></figcaption>
</figure>

View file

@ -0,0 +1,27 @@
{.section-title.accent.text-primary}
# Other special zones
## Making a "silent" zone
https://www.youtube.com/watch?v=z7XLo06o-ow
On your map, you can define special silent zones where nobody is allowed to talk. In these zones, users will not speak to each others, even if they are next to each others.
In order to create a silent zone:
* You must create a specific layer.
* In layer properties, you MUST add a boolean "`silent`" property. If the silent property is checked, the users are entering the silent zone when they walk on any tile of the layer.
## Playing sounds or background music
Your map can define special zones where a sound or background music will automatically be played.
In order to create a zone that triggers sounds/music:
* You must create a specific layer.
* In layer properties, you MUST add a "`playAudio`" property. The value of the property is a URL to an MP3 file that will be played. The URL can be relative to the URL of the map.
* You may use the boolean property "`audioLoop`" to make the sound loop (thanks captain obvious).
* If the "`audioVolume`" property is set, the audio player uses either the value of the property or the last volume set by the user - whichever is smaller. This property is a float from 0 to 1.0
{.alert.alert-info}
"`playAudioLoop`" is deprecated and should not be used anymore.

View file

@ -0,0 +1,183 @@
{.section-title.accent.text-primary}
# Using Typescript with the scripting API
{.alert.alert-info}
The easiest way to get started with writing scripts in Typescript is to use the
[Github map starter kit repository](https://github.com/thecodingmachine/workadventure-map-starter-kit). It comes with
Typescript enabled. If you are **not** using the "map starter kit", this page explains how to add Typescript to your
own scripts.
## The short story
In this page, we will assume you already know Typescript and know how to set it up with Webpack.
To work with the scripting API in Typescript, you will need the typings of the `WA` object. These typings can be downloaded from the `@workadventure/iframe-api-typings` package.
```console
$ npm install --save-dev @workadventure/iframe-api-typings
```
Furthermore, you need to make the global `WA` object available. To do this, edit the entry point of your project (usually, it is a file called `index.ts` in the root directory).
Add this line at the top of the file:
**index.ts**
```typescript
/// <reference path="../node_modules/@workadventure/iframe-api-typings/iframe_api.d.ts" />
```
From there, you should be able to use Typescript in your project.
## The long story
Below is a step by step guide explaining how to set up Typescript + Webpack along your WorkAdventure map.
In your map directory, start by adding a `package.json` file. This file will contain dependencies on Webpack, Typescript and the Workadventure typings:
**package.json**
```json
{
"devDependencies": {
"@workadventure/iframe-api-typings": "^1.2.1",
"eslint": "^7.24.0",
"html-webpack-plugin": "^5.3.1",
"ts-loader": "^8.1.0",
"ts-node": "^9.1.1",
"typescript": "^4.2.4",
"webpack": "^5.31.2",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"scripts": {
"start": "webpack serve --open",
"build": "webpack --config webpack.prod.js",
}
}
```
You can now install the dependencies:
```console
$ npm install
```
We now need to add a Webpack configuration file (for development mode). This Webpack file will:
* Start a local webserver that will be in charge of serving the map
* Compile Typescript into Javascript and serve it automatically
**webpack.config.js**
```js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.ts',
devtool: 'inline-source-map',
devServer: {
// The test webserver serves static files from the root directory.
// It comes with CORS enabled (important for WorkAdventure to be able to load the map)
static: {
directory: ".",
serveIndex: true,
watch: true,
},
host: 'localhost',
allowedHosts: "all",
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
}
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'script.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
}
};
```
We need to configure Typescript, using a `tsconfig.json` file.
**tsconfig.json**
```json
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"moduleResolution": "node",
"module": "CommonJS",
"target": "ES2015",
"declaration": false,
"downlevelIteration": true,
"jsx": "react",
"allowJs": true,
"strict": true
}
}
```
Create your entry point (the Typescript file at the root of your project).
**src/index.ts**
```typescript
/// <reference path="../node_modules/@workadventure/iframe-api-typings/iframe_api.d.ts" />
console.log('Hello world!');
```
The first comment line is important in order to get `WA` typings.
Now, you can start Webpack in dev mode!
```console
$ npm run start
```
This will automatically compile Typescript, and serve it (along the map) on your local webserver (so at `http://localhost:8080/script.js`). Please note that the `script.js` file is never written to the disk. So do not worry if you don't see it appearing, you need to "build" it to actually write it to the disk.
Final step, you must reference the script inside your map, by adding a `script` property at the root of your map:
<figure class="figure">
<img src="images/script_property.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The script property</figcaption>
</figure>
### Building the final script
We now have a correct development setup. But we still need to be able to build the production script from Typescript files. We are not going to use the development server in production. To do this, we will add an additional `webpack.prod.js` file.
**webpack.prod.js**
```javascript
const { merge } = require('webpack-merge');
const common = require('./webpack.config.js');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map'
});
```
This file will simply switch the Webpack config file in "production" mode. You can simply run:
```console
$ npm run build
```
and the `script.js` file will be generated in the `dist/` folder. Beware, you will need to move it at the root of map for it to be read by the map.

59
docs/maps/variables.md Normal file
View file

@ -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.
<div class="row">
<div class="col">
<img src="images/object_variable.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
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)

View file

@ -1,13 +1,13 @@
FROM thecodingmachine/workadventure-back-base:latest as builder
WORKDIR /var/www/messages
COPY --chown=docker:docker messages .
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
WORKDIR /usr/src
COPY messages .
RUN yarn install && yarn proto
# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL
FROM thecodingmachine/nodejs:14-apache
COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
COPY --from=builder --chown=docker:docker /usr/src/generated /var/www/html/src/Messages/generated
# Removing the iframe.html file from the final image as this adds a XSS attack.
# iframe.html is only in dev mode to circumvent a limitation

View file

@ -22,3 +22,5 @@ RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule "^[_@]/" "/index.html" [L]
RewriteRule "^register/" "/index.html" [L]
RewriteRule "^login" "/index.html" [L]
RewriteRule "^jwt/" "/index.html" [L]

View file

@ -34,7 +34,6 @@
<title>WorkAdventure</title>
</head>
<body id="body" style="margin: 0; background-color: #000">
<div class="main-container" id="main-container">
<!-- Create the editor container -->
<div id="game" class="game">
@ -62,31 +61,6 @@
<img src="resources/logos/close.svg"/>
</button>
</div>
<div id="audioplayerctrl" class="hidden">
<div class="audioplayer">
<button type="button" id="audioplayer_mute" class="fa fa-volump-up">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<g id="audioplayer_volume_icon_playing">
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
</g>
</svg>
</button>
<div class="audioplayer">
<input type="range" id="audioplayer_volume" min="0" max="1" step="0.025" value="1" />
</div>
</div>
<div class="audioplayer">
<label id="label-audioplayer_decrease_while_talking" for="audioplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
reduce in conversations
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
</label>
<div id="audioplayer" style="visibility: hidden"></div>
</div>
</div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active">

View file

@ -1,78 +0,0 @@
<style>
#gameMenu main{
margin-top: 15px;
}
#gameMenu button {
background-color: black;
color: white;
border-radius: 7px;
padding-bottom: 2px;
}
#gameMenu section {
margin: 10px;
}
section#socialLinks{
position: absolute;
margin-bottom: 0;
}
section#socialLinks img{
width: 32px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
@media only screen and (max-height: 700px) {
#gameMenu main {
display: flex;
flex-direction: row;
align-items: flex-end;
flex-wrap: wrap;
margin-top: 0;
}
#gameMenu section{
margin: 2px;
}
section#socialLinks{
position: relative;
}
}
</style>
<div id="gameMenu" hidden>
<main>
<section>
<button id="shareButton">Share url</button>
</section>
<section>
<button id="changeNameButton">Edit name</button>
</section>
<section>
<button id="changeSkinButton">Edit skin</button>
</section>
<section>
<button id="changeCompanionButton">Edit companion</button>
</section>
<section>
<button id="editGameSettingsButton">Settings</button>
</section>
<section>
<button id="toggleFullscreen">Toggle fullscreen</button>
</section>
<section>
<button id="enableNotification">Enable notifications</button>
</section>
<!-- TODO activate authentication -->
<section hidden>
<button id="oidcLogin">Oauth Login</button>
</section>
<section>
<button id="sparkButton">Create map</button>
</section>
<section id="adminConsoleSection" hidden>
<button id="adminConsoleButton">Admin console</button>
</section>
<section id="socialLinks" hidden>
<a class="not-button" href="https://www.facebook.com/workadventure.WA" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://twitter.com/Workadventure_" target="_blank"><img class="not-button" src="/resources/objects/twitter-icon.png"/></a>
</section>
</main>
</div>

View file

@ -1,28 +0,0 @@
<style>
#menuIcon button {
background-color: black;
color: white;
border-radius: 7px;
padding: 2px 8px;
}
#menuIcon button img{
width: 14px;
padding-top: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#menuIcon section {
margin: 10px;
}
@media only screen and (max-height: 700px) {
#menuIcon section {
margin: 2px;
}
}
</style>
<main id="menuIcon" hidden>
<section>
<button id="openMenuButton">
<img src="/static/images/menu.svg">
</button>
</section>
</main>

View file

@ -1,81 +0,0 @@
<style>
#gameQuality {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 20px auto 0;
width: 50vw;
max-width: 300px;
}
#gameQuality .cautiousText {
font-size: 50%;
}
#gameQuality h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#gameQuality select {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 30px;
transition: box-shadow 0.3s;
width: 100%;
}
#gameQuality section {
margin: 10px;
}
#gameQuality section.action{
text-align: center;
}
#gameQuality button {
margin: 10px;
background-color: black;
color: white;
border-radius: 7px;
padding-bottom: 4px;
}
#gameQuality button#gameQualityFormCancel {
background-color: #c7c7c700;
color: #292929;
}
</style>
<form id="gameQuality" hidden>
<section>
<h5>Game quality</h3>
<p class="cautiousText">(Editing these settings will restart the game)</p>
<select id="select-game-quality">
<option value="120">High video quality (120 fps)</option>
<option value="60">Medium video quality (60 fps, recommended)</option>
<option value="40">Minimum video quality (40 fps)</option>
<option value="20">Small video quality (20 fps)</option>
</select>
</section>
<section>
<h5>Video quality</h3>
<select id="select-video-quality">
<option value="30">High video quality (30 fps)</option>
<option value="20">Medium video quality (20 fps, recommended)</option>
<option value="10">Minimum video quality (10 fps)</option>
<option value="5">Small video quality (5 fps)</option>
</select>
</section>
<section class="action">
<button type="submit" id="gameQualityFormSubmit">Save</button>
<button type="reset" class="close" id="gameQualityFormCancel">Cancel</button>
</section>
</form>

View file

@ -1,115 +0,0 @@
<style>
#gameReport {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 2px auto 0;
width: 298px;
}
#gameReport h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#gameReport h3 {
margin: 0;
}
#gameReport textarea {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 100px;
transition: box-shadow 0.3s;
width: 100%;
}
#gameReport section {
margin: 10px;
}
#gameReport section.action{
text-align: center;
margin: 0;
}
#gameReport button {
margin-top: 10px;
font-size: 60%;
background-color: #dc3545;
color: white;
border-radius: 7px;
padding: 3px 10px 3px 10px;
}
#gameReport button#gameReportFormCancel {
background-color: #c7c7c700;
color: #292929;
display: block;
float: right;
}
#gameReport section a{
text-align: center;
font-size: 12px;
margin: 0 6px;
color: black;
}
#gameReport section h6,
#gameReport section h5{
margin: 1px;
}
#gameReport section.text-center{
text-align: center;
}
#gameReport p{
font-size: 8px;
margin: 3px 0 0 0;
}
#gameReport form p{
margin: 0px 70px;
}
#gameReport section p.err{
color: red;
display: none;
}
#gameReport section p.info{
display: none;
}
</style>
<main id="gameReport" hidden>
<section>
<button id="gameReportFormCancel">X</button>
<h1>Moderate <span id="nameReported"></span></h1>
<p id="askActionP">What action do you want to take?</p>
</section>
<section>
<h3>Block: </h3>
<p>Block any communication from and to this user. This can be reverted.</p>
<section class="action">
<button id="toggleBlockButton">Block this user</button>
</section>
</section>
<section id="reportSection">
<h3>Report: </h3>
<p>Send a report message to the administrators of this room. They may later ban this user.</p>
<form>
<section>
<h6>Your message: </h6>
<textarea type="text" name="report" id="gameReportInput"></textarea>
<p class="err" id="gameReportErr"></p>
</section>
<section class="action">
<button type="submit" id="gameReportFormSubmit">Report this user</button>
</section>
</form>
</section>
</main>

View file

@ -1,96 +0,0 @@
<style>
#gameShare {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 20px auto 0;
width: 50vw;
max-width: 400px;
}
#gameShare h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#gameShare input {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 30px;
transition: box-shadow 0.3s;
width: 100%;
}
#gameShare section {
margin: 10px;
}
#gameShare section.action{
text-align: center;
margin: 0;
}
#gameShare button {
margin: 10px;
background-color: black;
color: white;
border-radius: 7px;
padding-bottom: 4px;
width: 60px;
}
#gameShare button#gameShareFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#gameShare section a{
text-align: center;
font-size: 12px;
margin: 0 6px;
color: black;
}
#gameShare section h6,
#gameShare section h5{
margin: 1px;
}
#gameShare section.text-center{
text-align: center;
}
#gameShare section p{
font-size: 8px;
margin: 0;
}
#gameShare section p.err{
color: red;
display: none;
}
#gameShare section p.info{
display: none;
}
#gameShare section input#gameShareLink{
background-color: #a1a3a3;
}
</style>
<form id="gameShare" hidden>
<section class="text-center">
<h5>Share this link !</h5>
<p class="info" id="gameShareInfo"></p>
</section>
<section>
<h6>Link</h6>
<input type="text" name="email" id="gameShareLink" readonly>
</section>
<section class="action">
<button type="submit" id="gameShareFormSubmit">Copy</button>
<button type="submit" id="gameShareFormCancel">Close</button>
</section>
</form>

BIN
front/dist/resources/logos/tcm_full.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

BIN
front/dist/resources/logos/tcm_short.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -49,6 +49,7 @@
"phaser": "^3.54.0",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
"phaser3-rex-plugins": "^1.1.42",
"posthog-js": "^1.13.12",
"queue-typescript": "^1.0.1",
"quill": "1.3.6",
"quill-delta-to-html": "^0.12.0",

View file

@ -0,0 +1,61 @@
import {POSTHOG_API_KEY, POSTHOG_URL} from "../Enum/EnvironmentVariable";
class AnalyticsClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private posthogPromise: Promise<any>;
constructor() {
if (POSTHOG_API_KEY && POSTHOG_URL) {
this.posthogPromise = import('posthog-js').then(({default: posthog}) => {
posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_URL, disable_cookie: true });
return posthog;
});
} else {
this.posthogPromise = Promise.reject();
}
}
identifyUser(uuid: string) {
this.posthogPromise.then(posthog => {
posthog.identify(uuid, { uuid, wa: true });
}).catch();
}
loggedWithSso() {
this.posthogPromise.then(posthog => {
posthog.capture('wa-logged-sso');
}).catch();
}
loggedWithToken() {
this.posthogPromise.then(posthog => {
posthog.capture('wa-logged-token');
}).catch();
}
enteredRoom(roomId: string) {
this.posthogPromise.then(posthog => {
posthog.capture('$pageView', {roomId});
}).catch();
}
openedMenu() {
this.posthogPromise.then(posthog => {
posthog.capture('wa-opened-menu');
}).catch();
}
launchEmote(emote: string) {
this.posthogPromise.then(posthog => {
posthog.capture('wa-emote-launch', {emote});
}).catch();
}
enteredJitsi(roomName: string, roomId: string) {
this.posthogPromise.then(posthog => {
posthog.capture('wa-entered-jitsi', {roomName, roomId});
}).catch();
}
}
export const analyticsClient = new AnalyticsClient();

View file

@ -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.
*/

View file

@ -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.
*/

View file

@ -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.

View file

@ -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.
*/

View file

@ -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.

View file

@ -15,7 +15,6 @@ import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
@ -33,6 +32,7 @@ import type {
TriggerActionMessageEvent,
} from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -63,7 +63,8 @@ export type IframeEventMap = {
stopSound: null;
getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent;
registerMenu: MenuRegisterEvent;
unregisterMenu: UnregisterMenuEvent;
setTiles: SetTilesEvent;
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
};

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -1,5 +1,4 @@
import * as tg from "generic-type-guard";
import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
export const isSetVariableEvent = new tg.IsInterface()
.withProperties({

View file

@ -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.

View file

@ -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.
*/

View file

@ -1,26 +0,0 @@
import * as tg from "generic-type-guard";
import { Subject } from "rxjs";
export const isMenuItemRegisterEvent = new tg.IsInterface()
.withProperties({
menutItem: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a new menu item.
*/
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent,
})
.get();
const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem);
}

View file

@ -0,0 +1,31 @@
import * as tg from "generic-type-guard";
/**
* A message sent from a script to the game to remove a custom menu from the menu
*/
export const isUnregisterMenuEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
export type UnregisterMenuEvent = tg.GuardedType<typeof isUnregisterMenuEvent>;
export const isMenuRegisterOptions = new tg.IsInterface()
.withProperties({
allowApi: tg.isBoolean,
})
.get();
/**
* A message sent from a script to the game to add a custom menu from the menu
*/
export const isMenuRegisterEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
iframe: tg.isUnion(tg.isString, tg.isUndefined),
options: isMenuRegisterOptions,
})
.get();
export type MenuRegisterEvent = tg.GuardedType<typeof isMenuRegisterEvent>;

View file

@ -29,11 +29,12 @@ import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"],
@ -45,30 +46,18 @@ type AnswererCallback<T extends keyof IframeQueryMap> = (
* 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();
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
public readonly openPopupStream = this._openPopupStream.asObservable();
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
public readonly openTabStream = this._openTabStream.asObservable();
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _loadPageStream: Subject<string> = new Subject();
public readonly loadPageStream = this._loadPageStream.asObservable();
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
@ -93,12 +82,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
private readonly _unregisterMenuCommandStream: Subject<string> = new Subject();
public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable();
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable();
@ -224,7 +207,7 @@ class IframeListener {
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
scriptUtils.sendAnonymousChat(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
@ -260,17 +243,23 @@ class IframeListener {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
});
handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
this._modifyEmbeddedWebsiteStream.next(payload.data);
} else if (payload.type == "registerMenu" && isMenuRegisterEvent(payload.data)) {
const dataName = payload.data.name;
this.iframeCloseCallbacks.get(iframe)?.push(() => {
handleMenuUnregisterEvent(dataName);
});
handleMenuRegistrationEvent(
payload.data.name,
payload.data.iframe,
foundSrc,
payload.data.options
);
} else if (payload.type == "unregisterMenu" && isUnregisterMenuEvent(payload.data)) {
handleMenuUnregisterEvent(payload.data.name);
}
}
},
@ -293,57 +282,67 @@ class IframeListener {
this.iframes.delete(iframe);
}
registerScript(scriptUrl: string): void {
console.log("Loading map related script at ", scriptUrl);
registerScript(scriptUrl: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode (
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode (
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe);
iframe.addEventListener("load", () => {
resolve();
});
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
document.body.prepend(iframe);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc =
"<!doctype html>\n" +
"\n" +
'<html lang="en">\n' +
"<head>\n" +
'<script src="' +
window.location.protocol +
"//" +
window.location.host +
'/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe);
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc =
"<!doctype html>\n" +
"\n" +
'<html lang="en">\n' +
"<head>\n" +
'<script src="' +
window.location.protocol +
"//" +
window.location.host +
'/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
iframe.addEventListener("load", () => {
resolve();
});
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
});
}
private getBaseUrl(src: string, source: MessageEventSource | null): string {

View file

@ -1,4 +1,7 @@
import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore } from "../Stores/ChatStore";
import type { ChatEvent } from "./Events/ChatEvent";
class ScriptUtils {
public openTab(url: string) {
@ -16,6 +19,11 @@ class ScriptUtils {
public closeCoWebSite() {
coWebsiteManager.closeCoWebsite();
}
public sendAnonymousChat(chatEvent: ChatEvent) {
const userId = playersStore.addFacticePlayer(chatEvent.author);
chatMessagesStore.addExternalMessage(userId, chatEvent.message);
}
}
export const scriptUtils = new ScriptUtils();

View file

@ -1,38 +1,35 @@
import {sendToWorkadventure} from "../IframeApiContribution";
import type {LoadSoundEvent} from "../../Events/LoadSoundEvent";
import type {PlaySoundEvent} from "../../Events/PlaySoundEvent";
import type {StopSoundEvent} from "../../Events/StopSoundEvent";
import { sendToWorkadventure } from "../IframeApiContribution";
import type { LoadSoundEvent } from "../../Events/LoadSoundEvent";
import type { PlaySoundEvent } from "../../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../../Events/StopSoundEvent";
import SoundConfig = Phaser.Types.Sound.SoundConfig;
export class Sound {
constructor(private url: string) {
sendToWorkadventure({
"type": 'loadSound',
"data": {
type: "loadSound",
data: {
url: this.url,
} as LoadSoundEvent
} as LoadSoundEvent,
});
}
public play(config: SoundConfig) {
public play(config: SoundConfig | undefined) {
sendToWorkadventure({
"type": 'playSound',
"data": {
type: "playSound",
data: {
url: this.url,
config
} as PlaySoundEvent
config,
} as PlaySoundEvent,
});
return this.url;
}
public stop() {
sendToWorkadventure({
"type": 'stopSound',
"data": {
type: "stopSound",
data: {
url: this.url,
} as StopSoundEvent
} as StopSoundEvent,
});
return this.url;
}

View file

@ -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;
}

View file

@ -0,0 +1,17 @@
import { sendToWorkadventure } from "../IframeApiContribution";
export class Menu {
constructor(private menuName: string) {}
/**
* remove the menu
*/
public remove() {
sendToWorkadventure({
type: "unregisterMenu",
data: {
name: this.menuName,
},
});
}
}

View file

@ -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,
});
}
}

View file

@ -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<K> } = {}
export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback<K> } = {};
export function apiCallback<T extends keyof IframeResponseEventMap>(callbackData: IframeCallbackContribution<T>): IframeCallbackContribution<keyof IframeResponseEventMap> {
export function apiCallback<T extends keyof IframeResponseEventMap>(
callbackData: IframeCallbackContribution<T>
): IframeCallbackContribution<keyof IframeResponseEventMap> {
const iframeCallback = {
typeChecker: callbackData.typeChecker,
callback: callbackData.callback
callback: callbackData.callback,
} as IframeCallback<T>;
const newCallback = { [callbackData.type]: iframeCallback };
Object.assign(registeredCallbacks, newCallback)
Object.assign(registeredCallbacks, newCallback);
return callbackData as unknown as IframeCallbackContribution<keyof IframeResponseEventMap>;
}

View file

@ -62,6 +62,10 @@ export class WorkadventureStateCommands extends IframeApiContribution<Workadvent
return variables.get(key);
}
hasVariable(key: string): boolean {
return variables.has(key);
}
onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key);
if (subject === undefined) {
@ -85,6 +89,12 @@ const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
target.saveVariable(p.toString(), value);
return true;
},
has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
if (p in target) {
return true;
}
return target.hasVariable(p.toString());
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
export default proxyCommand;

View file

@ -6,6 +6,8 @@ import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescrip
import { Popup } from "./Ui/Popup";
import { ActionMessage } from "./Ui/ActionMessage";
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
import { Menu } from "./Ui/Menu";
import type { RequireOnlyOne } from "../types";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
@ -14,9 +16,18 @@ const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
Map<number, ButtonClickedCallback>
>();
const menus: Map<string, Menu> = new Map<string, Menu>();
const menuCallbacks: Map<string, (command: string) => void> = new Map();
const actionMessages = new Map<string, ActionMessage>();
interface MenuDescriptor {
callback?: (commandDescriptor: string) => void;
iframe?: string;
allowApi?: boolean;
}
export type MenuOptions = RequireOnlyOne<MenuDescriptor, "callback" | "iframe">;
interface ZonedPopupOptions {
zone: string;
objectLayerName?: string;
@ -52,6 +63,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
typeChecker: isMenuItemClickedEvent,
callback: (event) => {
const callback = menuCallbacks.get(event.menuItem);
const menu = menus.get(event.menuItem);
if (menu === undefined) {
throw new Error('Could not find menu named "' + event.menuItem + '"');
}
if (callback) {
callback(event.menuItem);
}
@ -104,14 +119,53 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
return popup;
}
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
type: "registerMenuCommand",
data: {
menutItem: commandDescriptor,
},
});
registerMenuCommand(commandDescriptor: string, options: MenuOptions | ((commandDescriptor: string) => void)): Menu {
const menu = new Menu(commandDescriptor);
if (typeof options === "function") {
menuCallbacks.set(commandDescriptor, options);
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
options: {
allowApi: false,
},
},
});
} else {
options.allowApi = options.allowApi === undefined ? options.iframe !== undefined : options.allowApi;
if (options.iframe !== undefined) {
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
iframe: options.iframe,
options: {
allowApi: options.allowApi,
},
},
});
} else if (options.callback !== undefined) {
menuCallbacks.set(commandDescriptor, options.callback);
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
options: {
allowApi: options.allowApi,
},
},
});
} else {
throw new Error(
"When adding a menu with WA.ui.registerMenuCommand, you must pass either an iframe or a callback"
);
}
}
menus.set(commandDescriptor, menu);
return menu;
}
displayBubble(): void {

4
front/src/Api/types.ts Normal file
View file

@ -0,0 +1,4 @@
export type RequireOnlyOne<T, keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, keys>> &
{
[K in keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<keys, K>, undefined>>;
}[keys];

View file

@ -1,4 +1,6 @@
<script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../Stores/MenuStore";
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte";
@ -23,10 +25,9 @@
import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
@ -37,6 +38,8 @@
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte"
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
export let game: Game;
@ -93,6 +96,21 @@
<LayoutManager></LayoutManager>
</div>
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<div>
<ReportMenu></ReportMenu>
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon></MenuIcon>
</div>
{/if}
{#if $menuVisiblilityStore}
<div>
<Menu></Menu>
</div>
{/if}
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay></VideoOverlay>
@ -100,11 +118,6 @@
<CameraControls></CameraControls>
</div>
{/if}
{#if $consoleGlobalMessageManagerVisibleStore}
<div>
<ConsoleGlobalMessageManager game={game}></ConsoleGlobalMessageManager>
</div>
{/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup></HelpCameraSettingsPopup>

View file

@ -1,17 +1,17 @@
<script lang="ts">
import audioImg from "../images/audio.svg";
import audioMuteImg from "../images/audio-mute.svg";
import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import {
audioManagerFileStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import {get} from "svelte/store";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import {onDestroy, onMount} from "svelte";
import { onDestroy, onMount } from "svelte";
let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement;
let audioPlayerVol: HTMLInputElement;
let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null;
@ -19,7 +19,11 @@
let decreaseWhileTalking: boolean = true;
onMount(() => {
unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{
volume = localUserStore.getAudioPlayerVolume();
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
changeVolume();
unsubscriberFileStore = audioManagerFileStore.subscribe(() => {
HTMLAudioPlayer.pause();
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
@ -49,14 +53,39 @@
}
})
function changeVolume() {
if (get(audioManagerVolumeStore).muted) {
audioPlayerVolumeIcon.classList.add('muted');
audioPlayerVol.value = "0";
} else {
audioPlayerVol.value = "" + volume;
audioPlayerVolumeIcon.classList.remove('muted');
if (volume < 0.3) {
audioPlayerVolumeIcon.classList.add('low');
} else if (volume < 0.7) {
audioPlayerVolumeIcon.classList.remove('low');
audioPlayerVolumeIcon.classList.add('mid');
} else {
audioPlayerVolumeIcon.classList.remove('low');
audioPlayerVolumeIcon.classList.remove('mid');
}
}
}
function onMute() {
audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted);
localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted);
const muted = !get(audioManagerVolumeStore).muted;
audioManagerVolumeStore.setMuted(muted);
localUserStore.setAudioPlayerMuted(muted);
changeVolume();
}
function setVolume() {
audioManagerVolumeStore.setVolume(volume)
localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume);
volume = parseFloat(audioPlayerVol.value);
audioManagerVolumeStore.setVolume(volume);
localUserStore.setAudioPlayerVolume(volume);
audioManagerVolumeStore.setMuted(false);
localUserStore.setAudioPlayerMuted(false);
changeVolume();
}
function setDecrease() {
@ -67,8 +96,27 @@
<div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume">
<img src={$audioManagerVolumeStore.muted ? audioMuteImg : audioImg} alt="player volume" on:click={onMute}>
<input type="range" min="0" max="1" step="0.025" bind:value={volume} on:change={setVolume}>
<span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon}
on:click={onMute}>
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<g id="audioplayer_volume_icon_playing_high">
<path
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
</g>
<g id="audioplayer_volume_icon_playing_mid">
<path
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
</g>
<g id="audioplayer_volume_icon_playing_low">
<path
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
</g>
</svg>
</span>
<input type="range" min="0" max="1" step="0.025" bind:this={audioPlayerVol} on:change={setVolume}>
</div>
<div class="audio-manager-reduce-conversation">
<label>
@ -86,34 +134,66 @@
<style lang="scss">
div.main-audio-manager.nes-container.is-rounded {
position: relative;
top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw);
padding: 3px 3px;
margin-left: auto;
margin-right: auto;
position: relative;
top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw);
padding: 3px 3px;
margin-left: auto;
margin-right: auto;
background-color: rgb(0,0,0,0.5);
display: grid;
grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
div.audio-manager-player-volume {
background-color: rgb(0, 0, 0, 0.5);
display: grid;
grid-template-columns: 50px 1fr;
grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
img {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
div.audio-manager-player-volume {
display: grid;
grid-template-columns: 50px 1fr;
span svg {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
}
}
}
section.audio-manager-file {
display: none;
}
section.audio-manager-file {
display: none;
}
#audioplayer_volume_icon_playing.muted {
#audioplayer_volume_icon_playing_low {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing_mid {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing_high {
visibility: hidden;
display: none;
}
}
#audioplayer_volume_icon_playing.low #audioplayer_volume_icon_playing_high {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing.low #audioplayer_volume_icon_playing_mid {
visibility: hidden;
display: none;
}
#audioplayer_volume_icon_playing.mid #audioplayer_volume_icon_playing_high {
visibility: hidden;
display: none;
}
}
</style>
</style>

View file

@ -1,6 +1,6 @@
<script lang="typescript">
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import {isSilentStore, requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg";
@ -12,6 +12,7 @@
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
import {LayoutMode} from "../WebRtc/LayoutManager";
import {peerStore} from "../Stores/PeerStore";
import {onDestroy} from "svelte";
function screenSharingClick(): void {
if ($requestedScreenSharingState === true) {
@ -44,6 +45,12 @@
$layoutModeStore = LayoutMode.Presentation;
}
}
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script>
<div>
@ -55,22 +62,22 @@
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
{/if}
</div>
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState}
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore || isSilent} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing">
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing">
{/if}
</div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}>
{#if $requestedCameraState}
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{#if $requestedCameraState && !isSilent}
<img src={cinemaImg} alt="Turn on webcam">
{:else}
<img src={cinemaCloseImg} alt="Turn off webcam">
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
{#if $requestedMicrophoneState}
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone">
{:else}
<img src={microphoneCloseImg} alt="Turn off microphone">

View file

@ -3,7 +3,7 @@
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte';
import {afterUpdate, beforeUpdate} from "svelte";
import {afterUpdate, beforeUpdate, onMount} from "svelte";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
let listDom: HTMLElement;
@ -15,6 +15,10 @@
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
});
onMount(() => {
listDom.scrollTo(0, listDom.scrollHeight);
})
afterUpdate(() => {
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
});

View file

@ -1,152 +0,0 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte";
import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import type { Game } from "../../Phaser/Game/Game";
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
export let game: Game;
let inputSendTextActive = true;
let uploadMusicActive = false;
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
let broadcastToWorld = false;
function closeConsoleGlobalMessage() {
consoleGlobalMessageManagerVisibleStore.set(false)
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeConsoleGlobalMessage();
}
}
function inputSendTextActivate() {
inputSendTextActive = true;
uploadMusicActive = false;
}
function inputUploadMusicActivate() {
uploadMusicActive = true;
inputSendTextActive = false;
}
function send() {
if (inputSendTextActive) {
handleSendText.sendTextMessage(broadcastToWorld);
}
if (uploadMusicActive) {
handleSendAudio.sendAudioMessage(broadcastToWorld);
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="console-global-message">
<div class="menu-console-global-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
<button type="button" class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
</div>
<div class="main-console-global-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<div class="title-console-global-message">
<h2>Global Message</h2>
<button type="button" class="nes-btn is-error" on:click|preventDefault={closeConsoleGlobalMessage}><i class="nes-icon close is-small"></i></button>
</div>
<div class="content-console-global-message">
{#if inputSendTextActive}
<InputTextGlobalMessage game={game} gameManager={gameManager} bind:handleSending={handleSendText}/>
{/if}
{#if uploadMusicActive}
<UploadAudioGlobalMessage game={game} gameManager={gameManager} bind:handleSending={handleSendAudio}/>
{/if}
</div>
<div class="footer-console-global-message">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
<span>Broadcast to all rooms of the world</span>
</label>
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button>
</div>
</div>
</div>
<style lang="scss">
.nes-container {
padding: 0 5px;
}
div.console-global-message {
top: 20vh;
width: 50vw;
height: 50vh;
position: relative;
display: flex;
flex-direction: row;
margin-left: auto;
margin-right: auto;
padding: 0;
pointer-events: auto;
div.menu-console-global-message {
flex: 1 1 auto;
max-width: 180px;
text-align: center;
background-color: #333333;
button {
width: 136px;
margin-bottom: 10px;
}
}
div.main-console-global-message {
flex: 1 1 auto;
display: flex;
flex-direction: column;
background-color: #333333;
div.title-console-global-message {
flex: 0 0 auto;
height: 50px;
margin-bottom: 10px;
text-align: center;
color: whitesmoke;
.nes-btn {
position: absolute;
top: 0;
right: 0;
}
}
div.content-console-global-message {
flex: 1 1 auto;
max-height: calc(100% - 120px);
}
div.footer-console-global-message {
height: 50px;
margin-top: 10px;
text-align: center;
label {
margin: 0;
position: absolute;
left: 0;
max-width: 30%;
}
}
}
}
</style>

View file

@ -3,10 +3,11 @@
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import {getNavigatorType, isAndroid as isAndroidFct, NavigatorType} from "../../WebRtc/DeviceUtils";
let isAndroid = window.navigator.userAgent.includes('Android');
let isFirefox = window.navigator.userAgent.includes('Firefox');
let isChrome = window.navigator.userAgent.includes('Chrome');
let isAndroid = isAndroidFct();
let isFirefox = getNavigatorType() === NavigatorType.firefox;
let isChrome = getNavigatorType() === NavigatorType.chrome;
function refresh() {
window.location.reload();

View file

@ -0,0 +1,147 @@
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import {onMount} from "svelte";
let gameScene = gameManager.getCurrentGameScene();
let HTMLShareLink: HTMLInputElement;
let expandedMapCopyright = false;
let expandedTilesetCopyright = false;
let mapName: string = "";
let mapDescription: string = "";
let mapCopyright: string = "The map creator did not declare a copyright for the map.";
let tilesetCopyright: string[] = [];
onMount(() => {
if (gameScene.mapFile.properties !== undefined) {
const propertyName = gameScene.mapFile.properties.find((property) => property.name === 'mapName')
if ( propertyName !== undefined && typeof propertyName.value === 'string') {
mapName = propertyName.value;
}
const propertyDescription = gameScene.mapFile.properties.find((property) => property.name === 'mapDescription')
if (propertyDescription !== undefined && typeof propertyDescription.value === 'string') {
mapDescription = propertyDescription.value;
}
const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === 'mapCopyright')
if (propertyCopyright !== undefined && typeof propertyCopyright.value === 'string') {
mapCopyright = propertyCopyright.value;
}
}
for (const tileset of gameScene.mapFile.tilesets) {
if (tileset.properties !== undefined) {
const propertyTilesetCopyright = tileset.properties.find((property) => property.name === 'tilesetCopyright')
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === 'string') {
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
}
}
}
})
function copyLink() {
HTMLShareLink.select();
document.execCommand('copy');
}
async function shareLink() {
const shareData = {url: location.toString()};
try {
await navigator.share(shareData);
} catch (err) {
console.error('Error: ' + err);
}
}
</script>
<div class="about-room-main">
<section class="share-url not-mobile">
<h3>Share the link of the room !</h3>
<input type="text" readonly bind:this={HTMLShareLink} value={location.toString()}>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
</section>
<section class="is-mobile">
<h3>Share the link of the room !</h3>
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section>
<h2>Information on the map</h2>
<section class="container-overflow">
<h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p>
<h3 class="nes-pointer hoverable" on:click={() => expandedMapCopyright = !expandedMapCopyright}>Copyrights of the map</h3>
<p class="string-HTML" hidden="{!expandedMapCopyright}">{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => expandedTilesetCopyright = !expandedTilesetCopyright}>Copyrights of the tilesets</h3>
<section hidden="{!expandedTilesetCopyright}">
{#each tilesetCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those tilesets have no license.</p>
{/each}
</section>
</section>
</div>
<style lang="scss">
.string-HTML{
white-space: pre-line;
}
div.about-room-main {
height: calc(100% - 56px);
section.share-url {
text-align: center;
margin-bottom: 20px;
input {
width: 85%;
border-radius: 32px;
padding: 3px;
}
input::selection {
background-color: #209cee;
}
}
section.is-mobile {
display: none;
}
h2, h3 {
width: 100%;
text-align: center;
}
h3.hoverable:hover {
background-color: #3c3e40;
border-radius: 32px;
}
section.container-overflow {
height: calc(100% - 220px);
margin: 0;
padding: 0;
overflow-y: auto;
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.about-room-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow {
height: calc(100% - 120px);
}
}
}
</style>

View file

@ -1,20 +1,15 @@
<script lang="ts">
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type { Game } from "../../Phaser/Game/Game";
import type { GameManager } from "../../Phaser/Game/GameManager";
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { gameManager } from "../../Phaser/Game/GameManager";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import uploadFile from "../images/music-file.svg";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
interface EventTargetFiles extends EventTarget {
files: Array<File>;
}
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let gameScene = gameManager.getCurrentGameScene();
let fileInput: HTMLInputElement;
let fileName: string;
let fileSize: string;
@ -45,7 +40,6 @@
}
inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
disableConsole();
}
}
@ -76,11 +70,6 @@
return '';
}
}
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
</script>
@ -105,24 +94,17 @@
img {
flex: 1 1 auto;
max-height: 80%;
margin-bottom: 20px;
}
p {
flex: 1 1 auto;
margin-bottom: 5px;
color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}

View file

@ -0,0 +1,15 @@
<script lang="ts">
import {CONTACT_URL} from "../../Enum/EnvironmentVariable";
</script>
<iframe title="contact" src="{CONTACT_URL}"></iframe>
<style lang="scss">
iframe {
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
}
</style>

View file

@ -0,0 +1,51 @@
<script lang="ts">
function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
}
function goToBuildingMap() {
const sparkHost = "https://workadventu.re/map-building";
window.open(sparkHost, "_blank");
}
</script>
<div class="create-map-main">
<section class="container-overflow">
<section>
<h3>Getting started</h3>
<p>
WorkAdventure allows you to create an online space to communicate spontaneously with others.
And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
</section>
<section>
<h3>Create your map</h3>
<p>You can also create your own custom map by following the step of the documentation.</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
</section>
</section>
</div>
<style lang="scss">
div.create-map-main {
height: calc(100% - 56px);
text-align: center;
section {
margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
}
</style>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte";
import {iframeListener} from "../../Api/IframeListener";
export let url: string;
export let allowApi: boolean;
let HTMLIframe: HTMLIFrameElement;
onMount( () => {
if (allowApi) {
iframeListener.registerIframe(HTMLIframe);
}
})
onDestroy( () => {
if (allowApi) {
iframeListener.unregisterIframe(HTMLIframe);
}
})
</script>
<iframe title="customSubMenu" src="{url}" bind:this={HTMLIframe}></iframe>
<style lang="scss">
iframe {
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
}
</style>

View file

@ -0,0 +1,118 @@
<script lang="ts">
import TextGlobalMessage from './TextGlobalMessage.svelte';
import AudioGlobalMessage from './AudioGlobalMessage.svelte';
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
let inputSendTextActive = true;
let uploadAudioActive = !inputSendTextActive;
let broadcastToWorld = false;
function activateInputText() {
inputSendTextActive = true;
uploadAudioActive = false;
}
function activateUploadAudio() {
inputSendTextActive = false;
uploadAudioActive = true;
}
function send() {
if (inputSendTextActive) {
handleSendText.sendTextMessage(broadcastToWorld);
}
if (uploadAudioActive) {
handleSendAudio.sendAudioMessage(broadcastToWorld);
}
}
</script>
<div class="global-message-main">
<div class="global-message-subOptions">
<section>
<button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={activateInputText}>Text</button>
</section>
<section>
<button type="button" class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}" on:click|preventDefault={activateUploadAudio}>Audio</button>
</section>
</div>
<div class="global-message-content">
{#if inputSendTextActive}
<TextGlobalMessage bind:handleSending={handleSendText}/>
{/if}
{#if uploadAudioActive}
<AudioGlobalMessage bind:handleSending={handleSendAudio}/>
{/if}
</div>
<div class="global-message-footer">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
<span>Broadcast to all rooms of the world</span>
</label>
<section>
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button>
</section>
</div>
</div>
<style lang="scss">
div.global-message-main {
height: calc(100% - 50px);
display: grid;
grid-template-rows: 15% 65% 20%;
div.global-message-subOptions {
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
section {
display: flex;
justify-content: center;
align-items: center;
}
}
div.global-message-footer {
margin-bottom: 10px;
display: grid;
grid-template-rows: 50% 50%;
section {
display: flex;
justify-content: center;
align-items: center;
}
label {
margin: 10px;
display: flex;
justify-content: center;
align-items: center;
span {
font-family: "Press Start 2P";
}
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.global-message-content {
height: calc(100% - 5px);
}
.global-message-footer {
margin-bottom: 0;
label {
width: calc(100% - 10px);
}
}
}
</style>

View file

@ -0,0 +1,181 @@
<script lang="typescript">
import {fly} from "svelte/transition";
import SettingsSubMenu from "./SettingsSubMenu.svelte";
import ProfileSubMenu from "./ProfileSubMenu.svelte";
import CreateMapSubMenu from "./CreateMapSubMenu.svelte";
import AboutRoomSubMenu from "./AboutRoomSubMenu.svelte";
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
import ContactSubMenu from "./ContactSubMenu.svelte";
import CustomSubMenu from "./CustomSubMenu.svelte"
import {
checkSubMenuToShow,
customMenuIframe,
menuVisiblilityStore,
SubMenusInterface,
subMenusStore
} from "../../Stores/MenuStore";
import {onDestroy, onMount} from "svelte";
import {get} from "svelte/store";
import type {Unsubscriber} from "svelte/store";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
let activeSubMenu: string = SubMenusInterface.settings;
let activeComponent: typeof SettingsSubMenu | typeof CustomSubMenu = SettingsSubMenu;
let props: { url: string, allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if(!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.settings);
}
})
checkSubMenuToShow();
switchMenu(SubMenusInterface.settings);
})
onDestroy(() => {
if(unsubscriberSubMenuStore) {
unsubscriberSubMenuStore();
}
})
function switchMenu(menu: string) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
activeSubMenu = menu;
switch (menu) {
case SubMenusInterface.settings:
activeComponent = SettingsSubMenu;
break;
case SubMenusInterface.profile:
activeComponent = ProfileSubMenu;
break;
case SubMenusInterface.createMap:
activeComponent = CreateMapSubMenu;
break;
case SubMenusInterface.aboutRoom:
activeComponent = AboutRoomSubMenu;
break;
case SubMenusInterface.globalMessages:
activeComponent = GlobalMessageSubMenu;
break;
case SubMenusInterface.contact:
activeComponent = ContactSubMenu;
break;
default:
const customMenu = customMenuIframe.get(menu);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu);
menuVisiblilityStore.set(false);
}
break;
}
} else throw ("There is no menu called " + menu);
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeMenu();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="menu-container-main">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<h2>Menu</h2>
<nav>
{#each $subMenusStore as submenu}
<button type="button" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" on:click|preventDefault={() => switchMenu(submenu)}>
{submenu}
</button>
{/each}
</nav>
</div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{activeSubMenu}</h2>
<svelte:component this={activeComponent} {...props}/>
</div>
</div>
<style lang="scss">
.nes-container {
padding: 5px;
}
div.menu-container-main {
--size-first-columns-grid: 200px;
font-family: "Press Start 2P";
pointer-events: auto;
height: 80%;
width: 75%;
top: 10%;
position: relative;
margin: auto;
display: grid;
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
grid-template-rows: 100%;
h2 {
text-align: center;
margin-bottom: 20px;
}
div.menu-nav-sidebar {
background-color: #333333;
color: whitesmoke;
nav button {
width: calc(100% - 10px);
margin-bottom: 10px;
}
}
div.menu-submenu-container {
background-color: #333333;
color: whitesmoke;
.nes-btn.is-error.close {
position: absolute;
top: -20px;
right: -20px;
}
}
}
@media only screen and (max-width: 800px) {
div.menu-container-main {
--size-first-columns-grid: 120px;
height: 70%;
top: 55px;
width: 100%;
font-size: 0.5em;
div.menu-nav-sidebar {
overflow-y: auto;
}
div.menu-submenu-container {
.nes-btn.is-error.close {
position: absolute;
top: -35px;
right: 0;
}
}
}
}
</style>

View file

@ -1,33 +1,40 @@
<script lang="typescript">
import logoWA from "../images/logo-WA-min.png"
import {menuVisiblilityStore} from "../../Stores/MenuStore";
import {get} from "svelte/store";
function showMenu(){
menuVisiblilityStore.set(!get(menuVisiblilityStore))
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Tab") {
showMenu();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<main class="menuIcon">
<section>
<button>
<img src="/static/images/menu.svg" alt="Open menu">
</button>
</section>
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu}>
</main>
<style lang="scss">
.menuIcon button {
background-color: black;
color: white;
border-radius: 7px;
padding: 2px 8px;
img {
width: 14px;
padding-top: 0;
/*cursor: url('/resources/logos/cursor_pointer.png'), pointer;*/
}
.menuIcon {
margin: 25px;
img {
pointer-events: auto;
width: 60px;
padding-top: 0;
}
.menuIcon section {
margin: 10px;
}
@media only screen and (max-height: 700px) {
.menuIcon section {
margin: 2px;
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.menuIcon {
margin: 3px;
img {
width: 50px;
}
}
}
</style>

View file

@ -0,0 +1,109 @@
<script lang="typescript">
import {gameManager} from "../../Phaser/Game/GameManager";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {PROFILE_URL} from "../../Enum/EnvironmentVariable";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {enableCameraSceneVisibilityStore} from "../../Stores/MediaStore";
function disableMenuStores(){
menuVisiblilityStore.set(false);
menuIconVisiblilityStore.set(false);
}
function openEditCompanionScene(){
disableMenuStores();
selectCompanionSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCompanionSceneName,new SelectCompanionScene());
}
function openEditNameScene(){
disableMenuStores();
loginSceneVisibleStore.set(true);
gameManager.leaveGame(LoginSceneName,new LoginScene());
}
function openEditSkinScene(){
disableMenuStores();
selectCharacterSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene());
}
function logOut(){
disableMenuStores();
loginSceneVisibleStore.set(true);
connectionManager.logout();
}
function getProfileUrl(){
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
}
function openEnableCameraScene(){
disableMenuStores();
enableCameraSceneVisibilityStore.showEnableCameraScene();
gameManager.leaveGame(EnableCameraSceneName,new EnableCameraScene());
}
</script>
<div class="customize-main">
{#if $userIsConnected}
<section>
{#if PROFILE_URL != undefined}
<iframe title="profile" src="{getProfileUrl()}"></iframe>
{/if}
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}>Log out</button>
</section>
{:else}
<section>
<a type="button" class="nes-btn" href="/login">Sign in</a>
</section>
{/if}
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>Edit Name</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>Edit Skin</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>Setup camera</button>
</section>
</div>
<style lang="scss">
div.customize-main{
section {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
iframe{
width: 100%;
height: 50vh;
border: none;
}
button {
height: 50px;
width: 250px;
}
}
}
@media only screen and (max-width: 800px) {
div.customize-main section button {
width: 130px;
}
}
</style>

View file

@ -0,0 +1,142 @@
<script lang="typescript">
import {localUserStore} from "../../Connexion/LocalUserStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {isMobile} from "../../Enum/EnvironmentVariable";
import {menuVisiblilityStore} from "../../Stores/MenuStore";
let fullscreen : boolean = localUserStore.getFullscreen();
let notification : boolean = localUserStore.getNotification() === 'granted';
let valueGame : number = localUserStore.getGameQualityValue();
let valueVideo : number = localUserStore.getVideoQualityValue();
let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
function saveSetting(){
if (valueGame !== previewValueGame) {
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame);
window.location.reload();
}
if (valueVideo !== previewValueVideo) {
previewValueVideo = valueVideo;
videoConstraintStore.setFrameRate(valueVideo);
}
closeMenu();
}
function changeFullscreen() {
const body = HtmlUtils.querySelectorOrFail('body');
if (body) {
if (document.fullscreenElement !== null && !fullscreen) {
document.exitFullscreen()
} else {
body.requestFullscreen();
}
localUserStore.setFullscreen(fullscreen);
}
}
function changeNotification() {
if (Notification.permission === 'granted') {
localUserStore.setNotification(notification ? 'granted' : 'denied');
} else {
Notification.requestPermission().then((response) => {
if (response === 'granted') {
localUserStore.setNotification(notification ? 'granted' : 'denied');
} else {
localUserStore.setNotification('denied');
notification = false;
}
})
}
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
</script>
<div class="settings-main" on:submit|preventDefault={saveSetting}>
<section>
<h3>Game quality</h3>
<div class="nes-select is-dark">
<select bind:value={valueGame}>
<option value="{120}">{isMobile() ? 'High (120 fps)' : 'High video quality (120 fps)'}</option>
<option value="{60}">{isMobile() ? 'Medium (60 fps)' : 'Medium video quality (60 fps, recommended)'}</option>
<option value="{40}">{isMobile() ? 'Minimum (40 fps)' : 'Minimum video quality (40 fps)'}</option>
<option value="{20}">{isMobile() ? 'Small (20 fps)' : 'Small video quality (20 fps)'}</option>
</select>
</div>
</section>
<section>
<h3>Video quality</h3>
<div class="nes-select is-dark">
<select bind:value={valueVideo}>
<option value="{30}">{isMobile() ? 'High (30 fps)' : 'High video quality (30 fps)'}</option>
<option value="{20}">{isMobile() ? 'Medium (20 fps)' : 'Medium video quality (20 fps, recommended)'}</option>
<option value="{10}">{isMobile() ? 'Minimum (10 fps)' : 'Minimum video quality (10 fps)'}</option>
<option value="{5}">{isMobile() ? 'Small (5 fps)' : 'Small video quality (5 fps)'}</option>
</select>
</div>
</section>
<section class="settings-section-save">
<p>(Saving these settings will restart the game)</p>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}>Save</button>
</section>
<section class="settings-section-noSaveOption">
<label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={fullscreen} on:change={changeFullscreen}/>
<span>Fullscreen</span>
</label>
<label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={notification} on:change={changeNotification}>
<span>Notifications</span>
</label>
</section>
</div>
<style lang="scss">
div.settings-main {
height: calc(100% - 40px);
overflow-y: auto;
section {
width: 100%;
padding: 20px 20px 0;
margin-bottom: 20px;
text-align: center;
div.nes-select select:focus {
outline: none;
}
}
section.settings-section-save {
text-align: center;
p {
margin: 16px 0;
}
}
section.settings-section-noSaveOption {
display: flex;
align-items: center;
flex-wrap: wrap;
label {
flex: 1 1 auto;
text-align: center;
margin: 0 0 15px;
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.settings-main {
section {
padding: 0;
}
}
}
</style>

View file

@ -1,8 +1,7 @@
<script lang="ts">
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {onDestroy, onMount} from "svelte";
import type { Game } from "../../Phaser/Game/Game";
import type { GameManager } from "../../Phaser/Game/GameManager";
import { menuInputFocusStore } from "../../Stores/MenuStore";
import { onDestroy, onMount } from "svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { Quill } from "quill";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
@ -25,20 +24,15 @@
[{'font': []}],
[{'align': []}],
['clean'],
['clean'], // remove formatting button
['link', 'image', 'video']
// remove formatting button
];
export let game: Game;
export let gameManager: GameManager;
const gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let quill: Quill;
let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
const gameScene = gameManager.getCurrentGameScene();
const MESSAGE_TYPE = AdminMessageEventTypes.admin;
let quill: Quill;
let QUILL_EDITOR: HTMLDivElement;
export const handleSending = {
sendTextMessage(broadcastToWorld: boolean) {
@ -55,39 +49,31 @@
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(textGlobalMessage);
disableConsole();
}
}
//Quill
onMount(async () => {
// Import quill
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(INPUT_CONSOLE_MESSAGE, {
quill = new Quill(QUILL_EDITOR, {
placeholder: 'Enter your message here...',
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
});
consoleGlobalMessageManagerFocusStore.set(true);
menuInputFocusStore.set(true);
});
onDestroy(() => {
consoleGlobalMessageManagerFocusStore.set(false);
menuInputFocusStore.set(false);
})
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
</script>
<section class="section-input-send-text">
<div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div>
<div class="input-send-text" bind:this={QUILL_EDITOR}></div>
</section>

View file

@ -1,27 +1,11 @@
<script lang="typescript">
import {localStreamStore} from "../Stores/MediaStore";
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {localStreamStore, isSilentStore} from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import {onDestroy} from "svelte";
function srcObject(node: HTMLVideoElement, stream: MediaStream) {
node.srcObject = stream;
return {
update(newStream: MediaStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream
}
}
}
}
import {srcObject} from "./Video/utils";
let stream : MediaStream|null;
/*$: {
if ($localStreamStore.type === 'success') {
stream = $localStreamStore.stream;
} else {
stream = null;
}
}*/
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
@ -33,14 +17,25 @@
onDestroy(unsubscribe);
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script>
<div>
<div class="video-container div-myCamVideo" class:hide={!$localStreamStore.constraints.video}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream }
<video class="myCamVideo" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
<div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline></video>
<SoundMeterWidget stream={stream}></SoundMeterWidget>
{/if}
</div>
<div class="is-silent" class:hide={isSilent}>
Silent zone
</div>
</div>

View file

@ -0,0 +1,44 @@
<script lang="ts">
import {blackListManager} from "../../WebRtc/BlackListManager";
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
import {onMount} from "svelte";
export let userUUID: string | undefined;
export let userName: string;
let userIsBlocked = false;
onMount(() => {
if (userUUID === undefined) {
userIsBlocked = false;
console.error("There is no user to block");
} else {
userIsBlocked = blackListManager.isBlackListed(userUUID);
}
})
function blockUser(): void {
if (userUUID === undefined) {
console.error("There is no user to block");
return;
}
blackListManager.isBlackListed(userUUID)
? blackListManager.cancelBlackList(userUUID)
: blackListManager.blackList(userUUID);
showReportScreenStore.set(userReportEmpty); //close the report menu
}
</script>
<div class="block-container">
<h3>Block</h3>
<p>Block any communication from and to {userName}. This can be reverted.</p>
<button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}>
{userIsBlocked ? 'Unblock this user' : 'Block this user'}
</button>
</div>
<style lang="scss">
div.block-container {
text-align: center;
}
</style>

View file

@ -0,0 +1,141 @@
<script lang="ts">
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
import BlockSubMenu from "./BlockSubMenu.svelte";
import ReportSubMenu from "./ReportSubMenu.svelte";
import {onDestroy, onMount} from "svelte";
import type {Unsubscriber} from "svelte/store";
import {playersStore} from "../../Stores/PlayersStore";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager";
import {get} from "svelte/store";
let blockActive = true;
let reportActive = !blockActive;
let anonymous: boolean = false;
let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid;
let userName = "No name";
let unsubscriber: Unsubscriber
onMount(() => {
unsubscriber = showReportScreenStore.subscribe((reportScreenStore) => {
if (reportScreenStore != null) {
userName = reportScreenStore.userName;
userUUID = playersStore.getPlayerById(reportScreenStore.userId)?.userUuid;
if (userUUID === undefined && reportScreenStore !== userReportEmpty) {
console.error("Could not find UUID for user with ID " + reportScreenStore.userId);
}
}
})
anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous;
})
onDestroy(() => {
if (unsubscriber) {
unsubscriber();
}
})
function close() {
showReportScreenStore.set(userReportEmpty);
}
function activateBlock() {
blockActive = true;
reportActive = false;
}
function activateReport() {
blockActive = false;
reportActive = true;
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
close();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="report-menu-main nes-container is-rounded">
<section class="report-menu-title">
<h2>Moderate {userName}</h2>
<section class="justify-center">
<button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
</section>
</section>
<section class="report-menu-action {anonymous ? 'hidden' : ''}">
<section class="justify-center">
<button type="button" class="nes-btn {blockActive ? 'is-disabled' : ''}" on:click|preventDefault={activateBlock}>Block</button>
</section>
<section class="justify-center">
<button type="button" class="nes-btn {reportActive ? 'is-disabled' : ''}" on:click|preventDefault={activateReport}>Report</button>
</section>
</section>
<section class="report-menu-content">
{#if blockActive}
<BlockSubMenu userUUID="{userUUID}" userName="{userName}"/>
{:else if reportActive}
<ReportSubMenu userUUID="{userUUID}"/>
{:else }
<p>ERROR : There is no action selected.</p>
{/if}
</section>
</div>
<style lang="scss">
.nes-container {
padding: 5px;
}
section.justify-center {
display: flex;
justify-content: center;
align-items: center;
}
div.report-menu-main {
font-family: "Press Start 2P";
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
height: 70vh;
width: 50vw;
top: 10vh;
margin: auto;
section.report-menu-title {
display: grid;
grid-template-columns: calc(100% - 45px) 40px;
margin-bottom: 20px;
h2 {
display: flex;
justify-content: center;
align-items: center;
}
}
section.report-menu-action {
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
}
section.report-menu-action.hidden {
display: none;
}
}
@media only screen and (max-width: 800px) {
div.report-menu-main {
top: 21vh;
height: 60vh;
width: 100vw;
font-size: 0.5em;
}
}
</style>

View file

@ -0,0 +1,55 @@
<script lang="ts">
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
import {gameManager} from "../../Phaser/Game/GameManager";
export let userUUID: string | undefined;
let reportMessage: string;
let hiddenError = true;
function submitReport() {
if (reportMessage === '') {
hiddenError = true;
} else {
hiddenError = false;
if( userUUID === undefined) {
console.error('User UUID is not valid.');
return;
}
gameManager.getCurrentGameScene().connection?.emitReportPlayerMessage(userUUID, reportMessage);
showReportScreenStore.set(userReportEmpty)
}
}
</script>
<div class="report-container-main">
<h3>Report</h3>
<p>Send a report message to the administrators of this room. They may later ban this user.</p>
<form>
<section>
<label>
<span>Your message: </span>
<textarea type="text" class="nes-textarea" bind:value={reportMessage}></textarea>
</label>
<p hidden="{hiddenError}">Report message cannot to be empty.</p>
</section>
<section>
<button type="submit" class="nes-btn is-error" on:click={submitReport}>Report this user</button>
</section>
</form>
</div>
<style lang="scss">
div.report-container-main {
text-align: center;
textarea {
height: clamp(100px, 15vh, 300px);
}
}
@media only screen and (max-height: 630px) {
div.report-container-main textarea {
height: 50px;
}
}
</style>

View file

@ -12,7 +12,9 @@
<div class="main-section">
{#if $videoFocusStore }
<MediaBox streamable={$videoFocusStore}></MediaBox>
{#key $videoFocusStore.uniqueId}
<MediaBox streamable={$videoFocusStore}></MediaBox>
{/key}
{/if}
</div>
<aside class="sidebar">

View file

@ -7,8 +7,6 @@
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
import {getColorByString, srcObject} from "./utils";
import {obtainedMediaConstraintIsMobileStore} from "../../Stores/MediaStore";
import {onDestroy} from "svelte";
export let peer: VideoPeer;
let streamStore = peer.streamStore;
@ -20,12 +18,6 @@
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
}
let isMobile : boolean|null;
const unsubscribe = obtainedMediaConstraintIsMobileStore.subscribe(value => {
isMobile = value;
});
onDestroy(unsubscribe);
</script>
<div class="video-container">
@ -45,7 +37,7 @@
<img alt="Report this user" src={reportImg}>
<span>Report/Block</span>
</button>
<video class:mobile="{isMobile === true}" use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
<img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Some files were not shown because too many files have changed in this diff Show more