Release v1.4.15 (#1411)

* audio player volume improvements

* Add workaround for #932

* Bump striptags from 3.1.1 to 3.2.0 in /messages

Bumps [striptags](https://github.com/ericnorris/striptags) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/ericnorris/striptags/releases)
- [Commits](https://github.com/ericnorris/striptags/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: striptags
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add anthoer note for https://github.com/thecodingmachine/workadventure/issues/932#issuecomment-867562208

* WIP: svelte menu

* temp

* temp

* Bump tar from 4.4.13 to 4.4.15 in /back

Bumps [tar](https://github.com/npm/node-tar) from 4.4.13 to 4.4.15.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.13...v4.4.15)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* New menu svelte

* Migration of report menu in svelte

* Migration of registerCustomMenu for Menu in Svelte
Refactor subMenuStore
Suppression of old MenuScene and ReportMenu

* Suppression of HTML files that aren't use anymore

* New version of cache management (#1365)

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* migrate to svelte

* remove redundancy

* initial localUserStore volume

* Exit scene acess denied detected (#1369)

* Add auth token user to get right in admin and check if user have right

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update error show

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update token generation (#1372)

- Permit only decode token to get map details,
 - If user have token expired, set the token to null and reload the page. This feature will be updated when authentication stategy will be finished.

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* GameManager has an attribute scenePlugin

* GameManager has an attribute scenePlugin

* Suppression of gameManager in IframeListener

* fix deeployer

* fix deeployer

* Fixing enter/leave event not properly sent on adjacent zones

On adjacent zones, the zone leave event was not properly triggered when leaving a zone for the zone next to it.
Closes #1366

* First pass on css

* First pass on css

* Second pass on css and reportMenu

* Second pass on css and reportMenu

* Second pass on css and reportMenu

* Improving popup

If a popup message is empty, only the buttons will be displayed (not the container)

Unrelated: the Sound.play method in the API now accepts 0 arguments.

* Third pass on css and reportMenu

* Correction following test

* Player return a the same position when after editing his profile

* Player return a the same position when after editing his profile (same as reconnection)

* Contact page only if environment variable exist

* Execute scripts of the map after creating gameScene

* Rollback on createPromise switched to public

* Bump tar from 6.1.0 to 6.1.10 in /pusher

Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.10.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.10)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add iframe submenu by scripting API
Delete menu by scripting API

* Removing ts-ignore

* REVIEW : Migration Menu and Report Menu in Svelte (#1363)

* WIP: svelte menu

* temp

* temp

* New menu svelte

* Migration of report menu in svelte

* Migration of registerCustomMenu for Menu in Svelte
Refactor subMenuStore
Suppression of old MenuScene and ReportMenu

* Suppression of HTML files that aren't use anymore

* fix deeployer

* First pass on css

* First pass on css

* Second pass on css and reportMenu

* Second pass on css and reportMenu

* Second pass on css and reportMenu

* Third pass on css and reportMenu

* Correction following test

* Contact page only if environment variable exist

* Update service worker

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Change requested

* Change requested

Co-authored-by: kharhamel <oognic@gmail.com>
Co-authored-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Refactor to only have one function registerMenuCommand
When selected custom menu is removed, go to settings menu
Allow iframe in custom menu to use Scripting API
Return menu object when it is registered, can call remove function on it

* Correct bad change

* Add types file in API

* Add types file in API

* Fixing "has/in" on variables proxy object

When using WA.state, using `"myVariable" in WA.state` would always return false.
This is now fixed by adding a "has" method on the Proxy class.

Also, added a `WA.state.hasVariable` method.

* Add documentation
delete unused test map

* Properties changed via the Iframe API now trigger changes directly

Changes performed in WA.room.setPropertyLayer now have a real-time impact.
If the property is changed on a layer the current player is on, the changes will be triggered.

* documentation and CHANGELOG

* add possibility to set size of coWebsite and Jitsis via map property

* Update GameScene.ts

typo fixed

* Update CoWebsiteManager.ts

typos and style

* Update CoWebsiteManager.ts

yet another typo

* FIX: media tracks were not readded to a 3rd person in some situations

* fix ReportMenu (#1397)

* remove the package systeminformation from back

* Bump url-parse from 1.5.1 to 1.5.3 in /front

Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump tar from 4.4.15 to 4.4.19 in /back

Bumps [tar](https://github.com/npm/node-tar) from 4.4.15 to 4.4.19.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.15...v4.4.19)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump path-parse from 1.0.6 to 1.0.7 in /messages

Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* openTabPropertyKey (create new props in own file)

* Bump path-parse from 1.0.6 to 1.0.7 in /uploader

Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump path-parse from 1.0.6 to 1.0.7 in /front

Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump path-parse from 1.0.6 to 1.0.7 in /back

Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump path-parse from 1.0.6 to 1.0.7 in /maps

Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* FEATURE: improved the mediaStore code to disable tracks instead of deleting them

* Bump path-parse from 1.0.6 to 1.0.7 in /benchmark

Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* added jitsiTypes

* renamed

* Allowing variables nested in group layers

Up until this commit, variables nested in object layers inside group layers where not found by the front nor the back.
This PR changes analysis so that variables can be detected.

* FIX: fixed a circular dependancy in stores by rewriting createPeerStore() and createScreenSharingPeerStore()

* WIP: Bypass camera scene (#1337)

* Set new local camera setup variable

* Finish by pass video settings

 - TODO add button to update camera settings

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Merge branch 'develop' into jumpVideoCamera

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

# Conflicts:
#	front/src/Connexion/LocalUserStore.ts
#	front/src/Phaser/Components/Loader.ts
#	front/src/Phaser/Game/GameManager.ts
#	front/src/Phaser/Login/EnableCameraScene.ts

* Add menu to open enable camera scene

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Finish jump camera setup

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Active authentication Oauth (#1377)

* Active authentication Oauth

 - Google authentication
 - GitHub authentication
 - Linkedin authentication

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Finish connexion et get user info connexion

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Fix lint error

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Change the expires token for 30 days

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update connexion stratgey

 - Set last room when it will be created and not when connexion is openned
 - Add '/login' end point permit to logout and open iframe to log user
 - Add logout feature permit to logout in front

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Implement logout and revoke token with hydra

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Fix pull develop conflict

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Profile url (#1399)

* Create function that permit to get profile URL

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Continue profil user

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Add menu and logout button

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update last room use

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Profile callback permit to get url profile setting from admin

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Finish profile show

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Delete profileUrl will be not use today

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Correct lint

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update size of iframe

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Delete console log

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update feedback ARP

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Emote silent zone (#1342)

* Add an emote when the user is in silent zone

* Update silent icon strategy

* Update strategy for silent zone

 - Add svelte store
 - Show silent zone indication and replace camera

This update permit to hide silent zone when user is in Jitsi discussion

* Fix css silent zone

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Hotfix media constraint error

 - Create error to manage displayed warning when we try to access on media with no constraint video and audio
 - Fix disabled microphone if we try to active and we don't have right or there is an error.

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

Co-authored-by: Lurkars <git@8lh.de>
Co-authored-by: Guy Sheffer <guysoft@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: kharhamel <oognic@gmail.com>
Co-authored-by: GRL <g.lesniewski@thecodingmachine.com>
Co-authored-by: David Négrier <d.negrier@thecodingmachine.com>
Co-authored-by: GRL78 <80678534+GRL78@users.noreply.github.com>
Co-authored-by: ¯\_(ツ)_/¯ <tabascoeye@gmail.com>
Co-authored-by: Kharhamel <Kharhamel@users.noreply.github.com>
Co-authored-by: jonny <ga86lad@mytum.de>
This commit is contained in:
grégoire parant 2021-09-05 19:51:33 +02:00 committed by GitHub
parent 7fdbcde71c
commit bf1953fe22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
113 changed files with 3017 additions and 2689 deletions

View file

@ -1,5 +1,11 @@
## Version develop ## 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.
## Version 1.4.14
### Updates ### Updates
- New scripting API features : - New scripting API features :
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file. - 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 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 ### 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). 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", "homepage": "https://github.com/thecodingmachine/workadventure#readme",
"dependencies": { "dependencies": {
"@workadventure/tiled-map-type-guard": "^1.0.0", "@workadventure/tiled-map-type-guard": "^1.0.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"busboy": "^0.3.1", "busboy": "^0.3.1",
"circular-json": "^0.5.9", "circular-json": "^0.5.9",
@ -54,7 +54,6 @@
"prom-client": "^12.0.0", "prom-client": "^12.0.0",
"query-string": "^6.13.3", "query-string": "^6.13.3",
"redis": "^3.1.2", "redis": "^3.1.2",
"systeminformation": "^4.31.1",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
"uuidv4": "^6.0.7" "uuidv4": "^6.0.7"
}, },

View file

@ -1,7 +1,12 @@
/** /**
* Handles variables shared between the scripting API and the server. * 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 { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository"; import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient"; import { redisClient } from "./RedisClient";
@ -83,25 +88,33 @@ export class VariablesManager {
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> { private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
const objects = new Map<string, Variable>(); const objects = new Map<string, Variable>();
for (const layer of map.layers) { for (const layer of map.layers) {
if (layer.type === "objectgroup") { this.recursiveFindVariablesInLayer(layer, objects);
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));
}
}
}
} }
return 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 { private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = {}; const variable: Variable = {};

View file

@ -194,10 +194,10 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@workadventure/tiled-map-type-guard@^1.0.0": "@workadventure/tiled-map-type-guard@^1.0.2":
version "1.0.0" version "1.0.2"
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba" resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.2.tgz#4171550f6cd71be19791faef48360d65d698bcb0"
integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A== integrity sha512-RCtygGV5y9cb7QoyGMINBE9arM5pyXjkxvXgA5uXEv4GDbXKorhFim/rHgwbVR+eFnVF3rDgWbRnk3DIaHt+lQ==
dependencies: dependencies:
generic-type-guard "^3.4.1" generic-type-guard "^3.4.1"
@ -554,7 +554,7 @@ chokidar@^3.4.0:
optionalDependencies: optionalDependencies:
fsevents "~2.1.2" fsevents "~2.1.2"
chownr@^1.1.1: chownr@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
@ -1159,7 +1159,7 @@ fragment-cache@^0.2.1:
dependencies: dependencies:
map-cache "^0.2.2" map-cache "^0.2.2"
fs-minipass@^1.2.5: fs-minipass@^1.2.7:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== 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" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 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" version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== 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" safe-buffer "^5.1.2"
yallist "^3.0.0" yallist "^3.0.0"
minizlib@^1.2.1: minizlib@^1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
@ -1992,7 +1992,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2" for-in "^1.0.2"
is-extendable "^1.0.1" 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" version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== 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== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6: path-parse@^1.0.6:
version "1.0.6" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-type@^1.0.0: path-type@^1.0.0:
version "1.1.0" version "1.1.0"
@ -2578,7 +2578,7 @@ rxjs@^6.6.7:
dependencies: dependencies:
tslib "^1.9.0" 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" version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -2962,11 +2962,6 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" 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: table@^5.2.3:
version "5.4.6" version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" 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" string-width "^3.0.0"
tar@^4.4.2: tar@^4.4.2:
version "4.4.13" version "4.4.19"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==
dependencies: dependencies:
chownr "^1.1.1" chownr "^1.1.4"
fs-minipass "^1.2.5" fs-minipass "^1.2.7"
minipass "^2.8.6" minipass "^2.9.0"
minizlib "^1.2.1" minizlib "^1.3.3"
mkdirp "^0.5.0" mkdirp "^0.5.5"
safe-buffer "^5.1.2" safe-buffer "^5.2.1"
yallist "^3.0.3" yallist "^3.1.1"
tdigest@^0.1.1: tdigest@^0.1.1:
version "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" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
yallist@^3.0.0, yallist@^3.0.3: yallist@^3.0.0, yallist@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==

View file

@ -429,9 +429,9 @@
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
}, },
"path-parse": { "path-parse": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"path-type": { "path-type": {
"version": "1.1.0", "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" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
path-parse@^1.0.6: path-parse@^1.0.6:
version "1.0.6" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
path-type@^1.0.0: path-type@^1.0.0:
version "1.1.0" version "1.1.0"

View file

@ -101,6 +101,7 @@
}, },
"redis": { "redis": {
"image": "redis:6", "image": "redis:6",
"ports": [6379]
} }
}, },
"config": { "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.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.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.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,6 +9,7 @@ Moreover, `WA.state` functions can be used to persist this state across reloads.
``` ```
WA.state.saveVariable(key : string, data : unknown): void WA.state.saveVariable(key : string, data : unknown): void
WA.state.loadVariable(key : string) : unknown WA.state.loadVariable(key : string) : unknown
WA.state.hasVariable(key : string) : boolean
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
WA.state.[any property]: unknown WA.state.[any property]: unknown
``` ```

View file

@ -68,25 +68,53 @@ WA.room.onLeaveZone('myZone', () => {
### Add custom menu ### 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. 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 ```javascript
const menu = WA.ui.registerMenuCommand('menu test',
{
callback: () => {
WA.chat.sendChatMessage('test');
}
})
WA.ui.registerMenuCommand("test", () => { // Some time later, if you want to remove the menu:
WA.chat.sendChatMessage("test clicked", "menu cmd") menu.remove();
})
``` ```
<div class="col"> Please note that `registerMenuCommand` returns an object of the `Menu` class.
<img src="images/menu-command.png" class="figure-img img-fluid rounded" alt="" />
</div> The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called.
```javascript
class Menu {
/**
* Remove the menu
*/
remove() {};
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -34,7 +34,6 @@
<title>WorkAdventure</title> <title>WorkAdventure</title>
</head> </head>
<body id="body" style="margin: 0; background-color: #000"> <body id="body" style="margin: 0; background-color: #000">
<div class="main-container" id="main-container"> <div class="main-container" id="main-container">
<!-- Create the editor container --> <!-- Create the editor container -->
<div id="game" class="game"> <div id="game" class="game">
@ -62,31 +61,6 @@
<img src="resources/logos/close.svg"/> <img src="resources/logos/close.svg"/>
</button> </button>
</div> </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>
<div id="activeScreenSharing" class="active-screen-sharing active"> <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

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

View file

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

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 { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent"; 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 { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite"; import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"], query: IframeQueryMap[T]["query"],
@ -93,12 +94,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject(); private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable(); 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(); private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable(); public readonly playSoundStream = this._playSoundStream.asObservable();
@ -260,17 +255,23 @@ class IframeListener {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true; 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)) { } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data); this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
this._modifyEmbeddedWebsiteStream.next(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 +294,67 @@ class IframeListener {
this.iframes.delete(iframe); this.iframes.delete(iframe);
} }
registerScript(scriptUrl: string): void { registerScript(scriptUrl: string): Promise<void> {
console.log("Loading map related script at ", scriptUrl); return new Promise<void>((resolve, reject) => {
console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode ( // Using external iframe mode (
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none"; iframe.style.display = "none";
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl); 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. // 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-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation"); iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe); iframe.addEventListener("load", () => {
resolve();
});
this.scripts.set(scriptUrl, iframe); document.body.prepend(iframe);
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
// We are putting a sandbox on this script because it will run in the same domain as the main website. this.scripts.set(scriptUrl, iframe);
iframe.sandbox.add("allow-scripts"); this.registerIframe(iframe);
iframe.sandbox.add("allow-top-navigation-by-user-activation"); } 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); // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.srcdoc = iframe.sandbox.add("allow-scripts");
"<!doctype html>\n" + iframe.sandbox.add("allow-top-navigation-by-user-activation");
"\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";
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); iframe.addEventListener("load", () => {
this.registerIframe(iframe); resolve();
} });
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
});
} }
private getBaseUrl(src: string, source: MessageEventSource | null): string { private getBaseUrl(src: string, source: MessageEventSource | null): string {

View file

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

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

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

View file

@ -6,6 +6,8 @@ import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescrip
import { Popup } from "./Ui/Popup"; import { Popup } from "./Ui/Popup";
import { ActionMessage } from "./Ui/ActionMessage"; import { ActionMessage } from "./Ui/ActionMessage";
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
import { Menu } from "./Ui/Menu";
import type { RequireOnlyOne } from "../types";
let popupId = 0; let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>(); 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> Map<number, ButtonClickedCallback>
>(); >();
const menus: Map<string, Menu> = new Map<string, Menu>();
const menuCallbacks: Map<string, (command: string) => void> = new Map(); const menuCallbacks: Map<string, (command: string) => void> = new Map();
const actionMessages = new Map<string, ActionMessage>(); 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 { interface ZonedPopupOptions {
zone: string; zone: string;
objectLayerName?: string; objectLayerName?: string;
@ -52,6 +63,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
typeChecker: isMenuItemClickedEvent, typeChecker: isMenuItemClickedEvent,
callback: (event) => { callback: (event) => {
const callback = menuCallbacks.get(event.menuItem); 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) { if (callback) {
callback(event.menuItem); callback(event.menuItem);
} }
@ -104,14 +119,53 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
return popup; return popup;
} }
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { registerMenuCommand(commandDescriptor: string, options: MenuOptions | ((commandDescriptor: string) => void)): Menu {
menuCallbacks.set(commandDescriptor, callback); const menu = new Menu(commandDescriptor);
sendToWorkadventure({
type: "registerMenuCommand", if (typeof options === "function") {
data: { menuCallbacks.set(commandDescriptor, options);
menutItem: commandDescriptor, 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 { 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"> <script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../Stores/MenuStore";
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore"; import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte"; import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte"; import MyCamera from "./MyCamera.svelte";
@ -23,10 +25,9 @@
import AudioPlaying from "./UI/AudioPlaying.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte"; import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
import AdminMessage from "./TypeMessage/BanMessage.svelte"; import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte"; import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore"; import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
@ -37,6 +38,8 @@
import LayoutManager from "./LayoutManager/LayoutManager.svelte"; import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore"; import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte" import AudioManager from "./AudioManager/AudioManager.svelte"
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
export let game: Game; export let game: Game;
@ -93,6 +96,21 @@
<LayoutManager></LayoutManager> <LayoutManager></LayoutManager>
</div> </div>
{/if} {/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} {#if $gameOverlayVisibilityStore}
<div> <div>
<VideoOverlay></VideoOverlay> <VideoOverlay></VideoOverlay>
@ -100,11 +118,6 @@
<CameraControls></CameraControls> <CameraControls></CameraControls>
</div> </div>
{/if} {/if}
{#if $consoleGlobalMessageManagerVisibleStore}
<div>
<ConsoleGlobalMessageManager game={game}></ConsoleGlobalMessageManager>
</div>
{/if}
{#if $helpCameraSettingsVisibleStore} {#if $helpCameraSettingsVisibleStore}
<div> <div>
<HelpCameraSettingsPopup></HelpCameraSettingsPopup> <HelpCameraSettingsPopup></HelpCameraSettingsPopup>

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import audioImg from "../images/audio.svg";
import audioMuteImg from "../images/audio-mute.svg";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore"; import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import { import {
@ -12,6 +10,8 @@
import {onDestroy, onMount} from "svelte"; import {onDestroy, onMount} from "svelte";
let HTMLAudioPlayer: HTMLAudioElement; let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement;
let audioPlayerVol: HTMLInputElement;
let unsubscriberFileStore: Unsubscriber | null = null; let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null; let unsubscriberVolumeStore: Unsubscriber | null = null;
@ -19,6 +19,10 @@
let decreaseWhileTalking: boolean = true; let decreaseWhileTalking: boolean = true;
onMount(() => { onMount(() => {
volume = localUserStore.getAudioPlayerVolume();
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
setVolume();
unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{ unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{
HTMLAudioPlayer.pause(); HTMLAudioPlayer.pause();
HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop; HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
@ -52,9 +56,26 @@
function onMute() { function onMute() {
audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted); audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted);
localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted); localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted);
setVolume();
} }
function setVolume() { function setVolume() {
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');
}
}
audioManagerVolumeStore.setVolume(volume) audioManagerVolumeStore.setVolume(volume)
localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume); localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume);
} }
@ -67,8 +88,28 @@
<div class="main-audio-manager nes-container is-rounded"> <div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume"> <div class="audio-manager-player-volume">
<img src={$audioManagerVolumeStore.muted ? audioMuteImg : audioImg} alt="player volume" on:click={onMute}> <span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon}
<input type="range" min="0" max="1" step="0.025" bind:value={volume} on:change={setVolume}> 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:value={volume} bind:this={audioPlayerVol}
on:change={setVolume}>
</div> </div>
<div class="audio-manager-reduce-conversation"> <div class="audio-manager-reduce-conversation">
<label> <label>
@ -86,34 +127,66 @@
<style lang="scss"> <style lang="scss">
div.main-audio-manager.nes-container.is-rounded { div.main-audio-manager.nes-container.is-rounded {
position: relative; position: relative;
top: 0.5rem; top: 0.5rem;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw); width: clamp(200px, 15vw, 15vw);
padding: 3px 3px; padding: 3px 3px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
background-color: rgb(0,0,0,0.5); 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 {
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-rows: 50% 50%;
color: whitesmoke;
text-align: center;
pointer-events: auto;
img { div.audio-manager-player-volume {
height: 100%; display: grid;
width: calc(100% - 10px); grid-template-columns: 50px 1fr;
margin-right: 10px;
span svg {
height: 100%;
width: calc(100% - 10px);
margin-right: 10px;
}
} }
}
section.audio-manager-file { section.audio-manager-file {
display: none; 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"> <script lang="typescript">
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore"; 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 monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg"; import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg"; import cinemaImg from "./images/cinema.svg";
@ -12,6 +12,7 @@
import {layoutModeStore} from "../Stores/StreamableCollectionStore"; import {layoutModeStore} from "../Stores/StreamableCollectionStore";
import {LayoutMode} from "../WebRtc/LayoutManager"; import {LayoutMode} from "../WebRtc/LayoutManager";
import {peerStore} from "../Stores/PeerStore"; import {peerStore} from "../Stores/PeerStore";
import {onDestroy} from "svelte";
function screenSharingClick(): void { function screenSharingClick(): void {
if ($requestedScreenSharingState === true) { if ($requestedScreenSharingState === true) {
@ -44,6 +45,12 @@
$layoutModeStore = LayoutMode.Presentation; $layoutModeStore = LayoutMode.Presentation;
} }
} }
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div>
@ -55,22 +62,22 @@
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode"> <img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
{/if} {/if}
</div> </div>
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}> <div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore || isSilent} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState} {#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing"> <img src={monitorImg} alt="Start screen sharing">
{:else} {:else}
<img src={monitorCloseImg} alt="Stop screen sharing"> <img src={monitorCloseImg} alt="Stop screen sharing">
{/if} {/if}
</div> </div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}> <div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{#if $requestedCameraState} {#if $requestedCameraState && !isSilent}
<img src={cinemaImg} alt="Turn on webcam"> <img src={cinemaImg} alt="Turn on webcam">
{:else} {:else}
<img src={cinemaCloseImg} alt="Turn off webcam"> <img src={cinemaCloseImg} alt="Turn off webcam">
{/if} {/if}
</div> </div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}> <div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState} {#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone"> <img src={microphoneImg} alt="Turn on microphone">
{:else} {:else}
<img src={microphoneCloseImg} alt="Turn off microphone"> <img src={microphoneCloseImg} alt="Turn off microphone">

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

@ -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"> <script lang="ts">
import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type { Game } from "../../Phaser/Game/Game"; import { gameManager } from "../../Phaser/Game/GameManager";
import type { GameManager } from "../../Phaser/Game/GameManager";
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import uploadFile from "../images/music-file.svg"; import uploadFile from "../images/music-file.svg";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels"; import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
interface EventTargetFiles extends EventTarget { interface EventTargetFiles extends EventTarget {
files: Array<File>; files: Array<File>;
} }
export let game: Game; let gameScene = gameManager.getCurrentGameScene();
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
let fileName: string; let fileName: string;
let fileSize: string; let fileSize: string;
@ -45,7 +40,6 @@
} }
inputAudio.value = ''; inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(audioGlobalMessage); gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
disableConsole();
} }
} }
@ -76,11 +70,6 @@
return ''; return '';
} }
} }
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
</script> </script>
@ -105,24 +94,17 @@
img { img {
flex: 1 1 auto; flex: 1 1 auto;
max-height: 80%; max-height: 80%;
margin-bottom: 20px; margin-bottom: 20px;
} }
p { p {
flex: 1 1 auto;
margin-bottom: 5px; margin-bottom: 5px;
color: whitesmoke; color: whitesmoke;
font-size: 1rem; font-size: 1rem;
&.err { &.err {
color: #ce372b; color: #ce372b;
} }
} }
input { input {
display: none; 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,154 @@
<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 {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);
}
})
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 }}">
<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: 80vh;
width: 75vw;
top: 10vh;
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;
}
}
@media only screen and (max-width: 800px) {
div.menu-container-main {
--size-first-columns-grid: 120px;
height: 70vh;
top: 55px;
width: 100vw;
font-size: 0.5em;
}
}
</style>

View file

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

View file

@ -0,0 +1,116 @@
<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());
}
//TODO: Uncomment when login will be completely developed
/*function clickLogin() {
connectionManager.loadOpenIDScreen();
}*/
</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">Sing in</a>
</section>
{/if}
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>Edit Name</button>
<button type="button" class="nes-btn is-rounded" 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>
<!-- <section>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={clickLogin}>Login</button>
</section>-->
</div>
<style lang="scss">
div.customize-main{
section {
display: flex;
justify-content: center;
align-items: center;
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,140 @@
<script lang="typescript">
import {localUserStore} from "../../Connexion/LocalUserStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {isMobile} from "../../Enum/EnvironmentVariable";
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);
}
}
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;
}
})
}
}
</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);
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 {
--nb-noSaveOptions: 2; //number of sub-element in the section
display: grid;
grid-template-columns: calc(100% / var(--nb-noSaveOptions)) calc(100% / var(--nb-noSaveOptions)); //Same size for every sub-element
label {
text-align: center;
width: 100%;
margin: 0;
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.settings-main {
section {
padding: 0;
}
section.settings-section-noSaveOption {
height: 80px;
grid-template-columns: none;
grid-template-rows: calc(100% / var(--nb-noSaveOptions)) calc(100% / var(--nb-noSaveOptions)); //Same size for every sub-element;
}
}
}
</style>

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -7,6 +7,8 @@ import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser"; import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room"; import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker"; import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@ -15,6 +17,7 @@ class ConnectionManager {
private reconnectingTimeout: NodeJS.Timeout | null = null; private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false; private _unloading: boolean = false;
private authToken: string | null = null; private authToken: string | null = null;
private _currentRoom: Room | null = null;
private serviceWorker?: _ServiceWorker; private serviceWorker?: _ServiceWorker;
@ -30,28 +33,39 @@ class ConnectionManager {
} }
/** /**
* @return Promise<void> * TODO fix me to be move in game manager
*/ */
public loadOpenIDScreen(): Promise<void> { public loadOpenIDScreen() {
const state = localUserStore.generateState(); const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce(); const nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null); localUserStore.setAuthToken(null);
//TODO refactor this and don't realise previous call //TODO fix me to redirect this URL by pusher
return Axios.get(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`) if (!this._currentRoom || !this._currentRoom.iframeAuthentication) {
.then(() => { loginSceneVisibleIframeStore.set(false);
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`); return null;
}) }
.catch((err) => { const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}`;
console.error(err, "We don't have URL to regenerate authentication user"); window.location.assign(redirectUrl);
//TODO show modal login return redirectUrl;
window.location.reload();
});
} }
public logout() { /**
* Logout
*/
public async logout() {
//user logout, set connected store for menu at false
userIsConnected.set(false);
//Logout user in pusher and hydra
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/logout-callback`, { params: { token } }).then(
(res) => res.data
);
localUserStore.setAuthToken(null); localUserStore.setAuthToken(null);
window.location.reload();
//Go on login page can permit to clear token and start authentication process
window.location.assign("/login");
} }
/** /**
@ -60,8 +74,13 @@ class ConnectionManager {
public async initGameConnexion(): Promise<Room> { public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType(); const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType; this.connexionType = connexionType;
let room: Room | null = null; this._currentRoom = null;
if (connexionType === GameConnexionTypes.jwt) { if (connexionType === GameConnexionTypes.login) {
//TODO clear all cash and redirect on login scene (iframe)
localUserStore.setAuthToken(null);
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code"); const code = urlParams.get("code");
const state = urlParams.get("state"); const state = urlParams.get("state");
@ -71,14 +90,15 @@ class ConnectionManager {
if (!code) { if (!code) {
throw "No Auth code provided"; throw "No Auth code provided";
} }
const nonce = localUserStore.getNonce(); localUserStore.setCode(code);
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then( try {
(res) => res.data await this.checkAuthUserConnexion();
); } catch (err) {
localUserStore.setAuthToken(authToken); console.error(err);
this.authToken = authToken; this.loadOpenIDScreen();
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl())); }
urlManager.pushRoomIdToUrl(room); this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.register) { } else if (connexionType === GameConnexionTypes.register) {
//@deprecated //@deprecated
const organizationMemberToken = urlManager.getOrganizationToken(); const organizationMemberToken = urlManager.getOrganizationToken();
@ -92,7 +112,7 @@ class ConnectionManager {
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
room = await Room.createRoom( this._currentRoom = await Room.createRoom(
new URL( new URL(
window.location.protocol + window.location.protocol +
"//" + "//" +
@ -102,7 +122,7 @@ class ConnectionManager {
window.location.hash window.location.hash
) )
); );
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(this._currentRoom);
} else if ( } else if (
connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.anonymous ||
@ -112,12 +132,18 @@ class ConnectionManager {
//todo: add here some kind of warning if authToken has expired. //todo: add here some kind of warning if authToken has expired.
if (!this.authToken) { if (!this.authToken) {
await this.anonymousLogin(); await this.anonymousLogin();
} else {
try {
await this.checkAuthUserConnexion();
} catch (err) {
console.error(err);
}
} }
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
let roomPath: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL; roomPath = localUserStore.getLastRoomUrl();
//get last room path from cache api //get last room path from cache api
try { try {
const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi(); const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi();
@ -138,13 +164,13 @@ class ConnectionManager {
} }
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
room = await Room.createRoom(new URL(roomPath)); this._currentRoom = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) { if (this._currentRoom.textures != undefined && this._currentRoom.textures.length > 0) {
//check if texture was changed //check if texture was changed
if (this.localUser.textures.length === 0) { if (this.localUser.textures.length === 0) {
this.localUser.textures = room.textures; this.localUser.textures = this._currentRoom.textures;
} else { } else {
room.textures.forEach((newTexture) => { this._currentRoom.textures.forEach((newTexture) => {
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id); const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) { if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return; return;
@ -155,12 +181,12 @@ class ConnectionManager {
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
} }
} }
if (room == undefined) { if (this._currentRoom == undefined) {
return Promise.reject(new Error("Invalid URL")); return Promise.reject(new Error("Invalid URL"));
} }
this.serviceWorker = new _ServiceWorker(); this.serviceWorker = new _ServiceWorker();
return Promise.resolve(room); return Promise.resolve(this._currentRoom);
} }
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
@ -215,9 +241,6 @@ class ConnectionManager {
}); });
connection.onConnect((connect: OnConnectInterface) => { connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect); resolve(connect);
}); });
}).catch((err) => { }).catch((err) => {
@ -237,6 +260,34 @@ class ConnectionManager {
get getConnexionType() { get getConnexionType() {
return this.connexionType; return this.connexionType;
} }
async checkAuthUserConnexion() {
//set connected store for menu at false
userIsConnected.set(false);
const state = localUserStore.getState();
const code = localUserStore.getCode();
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
//user connected, set connected store for menu at true
userIsConnected.set(true);
}
get currentRoom() {
return this._currentRoom;
}
} }
export const connectionManager = new ConnectionManager(); export const connectionManager = new ConnectionManager();

View file

@ -1,5 +1,6 @@
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser"; import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { START_ROOM_URL } from "../Enum/EnvironmentVariable";
const playerNameKey = "playerName"; const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer"; const selectedPlayerKey = "selectedPlayer";
@ -16,6 +17,9 @@ const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken"; const authToken = "authToken";
const state = "state"; const state = "state";
const nonce = "nonce"; const nonce = "nonce";
const notification = "notificationPermission";
const code = "code";
const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache"; const cacheAPIIndex = "workavdenture-cache";
@ -124,7 +128,9 @@ class LocalUserStore {
}); });
} }
getLastRoomUrl(): string { getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? ""; return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
} }
getLastRoomUrlCacheApi(): Promise<string | undefined> { getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => { return caches.open(cacheAPIIndex).then((cache) => {
@ -143,6 +149,14 @@ class LocalUserStore {
return localStorage.getItem(authToken); return localStorage.getItem(authToken);
} }
setNotification(value: string): void {
localStorage.setItem(notification, value);
}
getNotification(): string | null {
return localStorage.getItem(notification);
}
generateState(): string { generateState(): string {
const newState = uuidv4(); const newState = uuidv4();
localStorage.setItem(state, newState); localStorage.setItem(state, newState);
@ -151,19 +165,32 @@ class LocalUserStore {
verifyState(value: string): boolean { verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state); const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value; return oldValue === value;
} }
getState(): string | null {
return localStorage.getItem(state);
}
generateNonce(): string { generateNonce(): string {
const newNonce = uuidv4(); const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce); localStorage.setItem(nonce, newNonce);
return newNonce; return newNonce;
} }
getNonce(): string | null { getNonce(): string | null {
const oldValue = localStorage.getItem(nonce); return localStorage.getItem(nonce);
localStorage.removeItem(nonce); }
return oldValue; setCode(value: string): void {
localStorage.setItem(code, value);
}
getCode(): string | null {
return localStorage.getItem(code);
}
setCameraSetup(cameraId: string) {
localStorage.setItem(cameraSetup, cameraId);
}
getCameraSetup(): { video: unknown; audio: unknown } | undefined {
const cameraSetupValues = localStorage.getItem(cameraSetup);
return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined;
} }
} }

View file

@ -14,6 +14,8 @@ export interface RoomRedirect {
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; public readonly isPublic: boolean;
private _authenticationMandatory: boolean = false;
private _iframeAuthentication?: string;
private _mapUrl: string | undefined; private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined; private _textures: CharacterTexture[] | undefined;
private instance: string | undefined; private instance: string | undefined;
@ -101,6 +103,8 @@ export class Room {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl); console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl; this._mapUrl = data.mapUrl;
this._textures = data.textures; this._textures = data.textures;
this._authenticationMandatory = data.authenticationMandatory || false;
this._iframeAuthentication = data.iframeAuthentication;
return new MapDetail(data.mapUrl, data.textures); return new MapDetail(data.mapUrl, data.textures);
} }
@ -186,4 +190,12 @@ export class Room {
} }
return this._mapUrl; return this._mapUrl;
} }
get authenticationMandatory(): boolean {
return this._authenticationMandatory;
}
get iframeAuthentication(): string | undefined {
return this._iframeAuthentication;
}
} }

View file

@ -78,6 +78,11 @@ export class RoomConnection implements RoomConnection {
* *
* @param token A JWT token containing the email of the user * @param token A JWT token containing the email of the user
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]" * @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
* @param name
* @param characterLayers
* @param position
* @param viewport
* @param companion
*/ */
public constructor( public constructor(
token: string | null, token: string | null,
@ -218,7 +223,7 @@ export class RoomConnection implements RoomConnection {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
this.closed = true; this.closed = true;
} else if (message.hasTokenexpiredmessage()) { } else if (message.hasTokenexpiredmessage()) {
connectionManager.loadOpenIDScreen(); connectionManager.logout();
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
} else if (message.hasWorldconnexionmessage()) { } else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());

View file

@ -18,6 +18,8 @@ export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true"; export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const NODE_ENV = process.env.NODE_ENV || "development"; export const NODE_ENV = process.env.NODE_ENV || "development";
export const CONTACT_URL = process.env.CONTACT_URL || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;

View file

@ -1,48 +1,63 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig; import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
import { DirtyScene } from "../Game/DirtyScene";
const LogoNameIndex: string = 'logoLoading'; const LogoNameIndex: string = "logoLoading";
const TextName: string = 'Loading...'; const TextName: string = "Loading...";
const LogoResource: string = 'resources/logos/logo.png'; const LogoResource: string = "resources/logos/logo.png";
const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59}; const LogoFrame: ImageFrameConfig = { frameWidth: 307, frameHeight: 59 };
export const addLoader = (scene: Phaser.Scene): void => { export const addLoader = (scene: Phaser.Scene): void => {
// If there is nothing to load, do not display the loader. // If there is nothing to load, do not display the loader.
if (scene.load.list.entries.length === 0) { if (scene.load.list.entries.length === 0) {
return; return;
} }
let loadingText: Phaser.GameObjects.Text|null = null; let loadingText: Phaser.GameObjects.Text | null = null;
const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3); const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3);
const loadingBarHeight: number = 16; const loadingBarHeight: number = 16;
const padding: number = 5; const padding: number = 5;
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => { const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
if(scene.load.textureManager.exists(LogoNameIndex)){ if (scene.load.textureManager.exists(LogoNameIndex)) {
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)); return res(
}else{ scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)
);
} else {
//add loading if logo image is not ready //add loading if logo image is not ready
loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName); loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName);
} }
scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame); scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if(loadingText){ if (loadingText) {
loadingText.destroy(); loadingText.destroy();
} }
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)); return res(
scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)
);
}); });
}); });
const progressContainer = scene.add.graphics(); const progressContainer = scene.add.graphics();
const progress = scene.add.graphics(); const progress = scene.add.graphics();
progressContainer.fillStyle(0x444444, 0.8); progressContainer.fillStyle(0x444444, 0.8);
progressContainer.fillRect((scene.game.renderer.width - loadingBarWidth) / 2 - padding, scene.game.renderer.height / 2 + 50 - padding, loadingBarWidth + padding * 2, loadingBarHeight + padding * 2); progressContainer.fillRect(
(scene.game.renderer.width - loadingBarWidth) / 2 - padding,
scene.game.renderer.height / 2 + 50 - padding,
loadingBarWidth + padding * 2,
loadingBarHeight + padding * 2
);
scene.load.on('progress', (value: number) => { scene.load.on("progress", (value: number) => {
progress.clear(); progress.clear();
progress.fillStyle(0xBBBBBB, 1); progress.fillStyle(0xbbbbbb, 1);
progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight); progress.fillRect(
(scene.game.renderer.width - loadingBarWidth) / 2,
scene.game.renderer.height / 2 + 50,
loadingBarWidth * value,
loadingBarHeight
);
}); });
scene.load.on('complete', () => { scene.load.on("complete", () => {
if(loadingText){ if (loadingText) {
loadingText.destroy(); loadingText.destroy();
} }
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => { promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
@ -50,5 +65,14 @@ export const addLoader = (scene: Phaser.Scene): void => {
}); });
progress.destroy(); progress.destroy();
progressContainer.destroy(); progressContainer.destroy();
if (scene instanceof DirtyScene) {
scene.markDirty();
}
}); });
} };
export const removeLoader = (scene: Phaser.Scene): void => {
if (scene.load.textureManager.exists(LogoNameIndex)) {
scene.load.textureManager.remove(LogoNameIndex);
}
};

View file

@ -1,29 +1,30 @@
import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation"; import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
import {SpeechBubble} from "./SpeechBubble"; import { SpeechBubble } from "./SpeechBubble";
import Text = Phaser.GameObjects.Text; import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container; import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import {TextureError} from "../../Exception/TextureError"; import { TextureError } from "../../Exception/TextureError";
import {Companion} from "../Companion/Companion"; import { Companion } from "../Companion/Companion";
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { isSilentStore } from "../../Stores/MediaStore";
const playerNameY = - 25; const playerNameY = -25;
interface AnimationData { interface AnimationData {
key: string; key: string;
frameRate: number; frameRate: number;
repeat: number; repeat: number;
frameModel: string; //todo use an enum frameModel: string; //todo use an enum
frames : number[] frames: number[];
} }
const interactiveRadius = 35; const interactiveRadius = 35;
export abstract class Character extends Container { export abstract class Character extends Container {
private bubble: SpeechBubble|null = null; private bubble: SpeechBubble | null = null;
private readonly playerName: Text; private readonly playerName: Text;
public PlayerValue: string; public PlayerValue: string;
public sprites: Map<string, Sprite>; public sprites: Map<string, Sprite>;
@ -32,35 +33,41 @@ export abstract class Character extends Container {
private invisible: boolean; private invisible: boolean;
public companion?: Companion; public companion?: Companion;
private emote: Phaser.GameObjects.Sprite | null = null; private emote: Phaser.GameObjects.Sprite | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null; private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene; scene: GameScene;
constructor(scene: GameScene, constructor(
x: number, scene: GameScene,
y: number, x: number,
texturesPromise: Promise<string[]>, y: number,
name: string, texturesPromise: Promise<string[]>,
direction: PlayerAnimationDirections, name: string,
moving: boolean, direction: PlayerAnimationDirections,
frame: string | number, moving: boolean,
isClickable: boolean, frame: string | number,
companion: string|null, isClickable: boolean,
companionTexturePromise?: Promise<string> companion: string | null,
companionTexturePromise?: Promise<string>
) { ) {
super(scene, x, y/*, texture, frame*/); super(scene, x, y /*, texture, frame*/);
this.scene = scene; this.scene = scene;
this.PlayerValue = name; this.PlayerValue = name;
this.invisible = true this.invisible = true;
this.sprites = new Map<string, Sprite>(); this.sprites = new Map<string, Sprite>();
//textures are inside a Promise in case they need to be lazyloaded before use. //textures are inside a Promise in case they need to be lazyloaded before use.
texturesPromise.then((textures) => { texturesPromise.then((textures) => {
this.addTextures(textures, frame); this.addTextures(textures, frame);
this.invisible = false this.invisible = false;
}) });
this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"}); this.playerName = new Text(scene, 0, playerNameY, name, {
fontFamily: '"Press Start 2P"',
fontSize: "8px",
strokeThickness: 2,
stroke: "gray",
});
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX); this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName); this.add(this.playerName);
@ -71,18 +78,17 @@ export abstract class Character extends Container {
useHandCursor: true, useHandCursor: true,
}); });
this.on('pointerover',() => { this.on("pointerover", () => {
this.getOutlinePlugin()?.add(this.playerName, { this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2, thickness: 2,
outlineColor: 0xffff00 outlineColor: 0xffff00,
}); });
this.scene.markDirty(); this.scene.markDirty();
}); });
this.on('pointerout',() => { this.on("pointerout", () => {
this.getOutlinePlugin()?.remove(this.playerName); this.getOutlinePlugin()?.remove(this.playerName);
this.scene.markDirty(); this.scene.markDirty();
}) });
} }
scene.add.existing(this); scene.add.existing(this);
@ -97,38 +103,38 @@ export abstract class Character extends Container {
this.playAnimation(direction, moving); this.playAnimation(direction, moving);
if (typeof companion === 'string') { if (typeof companion === "string") {
this.addCompanion(companion, companionTexturePromise); this.addCompanion(companion, companionTexturePromise);
} }
} }
private getOutlinePlugin(): OutlinePipelinePlugin|undefined { private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined; return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
} }
public addCompanion(name: string, texturePromise?: Promise<string>): void { public addCompanion(name: string, texturePromise?: Promise<string>): void {
if (typeof texturePromise !== 'undefined') { if (typeof texturePromise !== "undefined") {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
} }
} }
public addTextures(textures: string[], frame?: string | number): void { public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) { for (const texture of textures) {
if(this.scene && !this.scene.textures.exists(texture)){ if (this.scene && !this.scene.textures.exists(texture)) {
throw new TextureError('texture not found'); throw new TextureError("texture not found");
} }
const sprite = new Sprite(this.scene, 0, 0, texture, frame); const sprite = new Sprite(this.scene, 0, 0, texture, frame);
this.add(sprite); this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => { this.getPlayerAnimations(texture).forEach((d) => {
this.scene.anims.create({ this.scene.anims.create({
key: d.key, key: d.key,
frames: this.scene.anims.generateFrameNumbers(d.frameModel, {frames: d.frames}), frames: this.scene.anims.generateFrameNumbers(d.frameModel, { frames: d.frames }),
frameRate: d.frameRate, frameRate: d.frameRate,
repeat: d.repeat repeat: d.repeat,
}); });
}) });
// Needed, otherwise, animations are not handled correctly. // Needed, otherwise, animations are not handled correctly.
if(this.scene) { if (this.scene) {
this.scene.sys.updateList.add(sprite); this.scene.sys.updateList.add(sprite);
} }
this.sprites.set(texture, sprite); this.sprites.set(texture, sprite);
@ -136,68 +142,77 @@ export abstract class Character extends Container {
} }
private getPlayerAnimations(name: string): AnimationData[] { private getPlayerAnimations(name: string): AnimationData[] {
return [{ return [
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`, {
frameModel: name, key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frames: [0, 1, 2, 1], frameModel: name,
frameRate: 10, frames: [0, 1, 2, 1],
repeat: -1 frameRate: 10,
}, { repeat: -1,
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`, },
frameModel: name, {
frames: [3, 4, 5, 4], key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frameRate: 10, frameModel: name,
repeat: -1 frames: [3, 4, 5, 4],
}, { frameRate: 10,
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`, repeat: -1,
frameModel: name, },
frames: [6, 7, 8, 7], {
frameRate: 10, key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
repeat: -1 frameModel: name,
}, { frames: [6, 7, 8, 7],
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`, frameRate: 10,
frameModel: name, repeat: -1,
frames: [9, 10, 11, 10], },
frameRate: 10, {
repeat: -1 key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
},{ frameModel: name,
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`, frames: [9, 10, 11, 10],
frameModel: name, frameRate: 10,
frames: [1], repeat: -1,
frameRate: 10, },
repeat: 1 {
}, { key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`, frameModel: name,
frameModel: name, frames: [1],
frames: [4], frameRate: 10,
frameRate: 10, repeat: 1,
repeat: 1 },
}, { {
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`, key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frameModel: name, frameModel: name,
frames: [7], frames: [4],
frameRate: 10, frameRate: 10,
repeat: 1 repeat: 1,
}, { },
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`, {
frameModel: name, key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frames: [10], frameModel: name,
frameRate: 10, frames: [7],
repeat: 1 frameRate: 10,
}]; repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [10],
frameRate: 10,
repeat: 1,
},
];
} }
protected playAnimation(direction : PlayerAnimationDirections, moving: boolean): void { protected playAnimation(direction: PlayerAnimationDirections, moving: boolean): void {
if (this.invisible) return; if (this.invisible) return;
for (const [texture, sprite] of this.sprites.entries()) { for (const [texture, sprite] of this.sprites.entries()) {
if (!sprite.anims) { if (!sprite.anims) {
console.error('ANIMS IS NOT DEFINED!!!'); console.error("ANIMS IS NOT DEFINED!!!");
return; return;
} }
if (moving && (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== direction)) { if (moving && (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== direction)) {
sprite.play(texture+'-'+direction+'-'+PlayerAnimationTypes.Walk, true); sprite.play(texture + "-" + direction + "-" + PlayerAnimationTypes.Walk, true);
} else if (!moving) { } else if (!moving) {
sprite.anims.play(texture + '-' + direction + '-'+PlayerAnimationTypes.Idle, true); sprite.anims.play(texture + "-" + direction + "-" + PlayerAnimationTypes.Idle, true);
} }
} }
} }
@ -205,7 +220,7 @@ export abstract class Character extends Container {
protected getBody(): Phaser.Physics.Arcade.Body { protected getBody(): Phaser.Physics.Arcade.Body {
const body = this.body; const body = this.body;
if (!(body instanceof Phaser.Physics.Arcade.Body)) { if (!(body instanceof Phaser.Physics.Arcade.Body)) {
throw new Error('Container does not have arcade body'); throw new Error("Container does not have arcade body");
} }
return body; return body;
} }
@ -216,16 +231,20 @@ export abstract class Character extends Container {
body.setVelocity(x, y); body.setVelocity(x, y);
// up or down animations are prioritized over left and right // up or down animations are prioritized over left and right
if (body.velocity.y < 0) { //moving up if (body.velocity.y < 0) {
//moving up
this.lastDirection = PlayerAnimationDirections.Up; this.lastDirection = PlayerAnimationDirections.Up;
this.playAnimation(PlayerAnimationDirections.Up, true); this.playAnimation(PlayerAnimationDirections.Up, true);
} else if (body.velocity.y > 0) { //moving down } else if (body.velocity.y > 0) {
//moving down
this.lastDirection = PlayerAnimationDirections.Down; this.lastDirection = PlayerAnimationDirections.Down;
this.playAnimation(PlayerAnimationDirections.Down, true); this.playAnimation(PlayerAnimationDirections.Down, true);
} else if (body.velocity.x > 0) { //moving right } else if (body.velocity.x > 0) {
//moving right
this.lastDirection = PlayerAnimationDirections.Right; this.lastDirection = PlayerAnimationDirections.Right;
this.playAnimation(PlayerAnimationDirections.Right, true); this.playAnimation(PlayerAnimationDirections.Right, true);
} else if (body.velocity.x < 0) { //moving left } else if (body.velocity.x < 0) {
//moving left
this.lastDirection = PlayerAnimationDirections.Left; this.lastDirection = PlayerAnimationDirections.Left;
this.playAnimation(PlayerAnimationDirections.Left, true); this.playAnimation(PlayerAnimationDirections.Left, true);
} }
@ -237,32 +256,39 @@ export abstract class Character extends Container {
} }
} }
stop(){ stop() {
this.getBody().setVelocity(0, 0); this.getBody().setVelocity(0, 0);
this.playAnimation(this.lastDirection, false); this.playAnimation(this.lastDirection, false);
} }
say(text: string) { say(text: string) {
if (this.bubble) return; if (this.bubble) return;
this.bubble = new SpeechBubble(this.scene, this, text) this.bubble = new SpeechBubble(this.scene, this, text);
setTimeout(() => { setTimeout(() => {
if (this.bubble !== null) { if (this.bubble !== null) {
this.bubble.destroy(); this.bubble.destroy();
this.bubble = null; this.bubble = null;
} }
}, 3000) }, 3000);
} }
destroy(): void { destroy(): void {
for (const sprite of this.sprites.values()) { for (const sprite of this.sprites.values()) {
if(this.scene) { if (this.scene) {
this.scene.sys.updateList.remove(sprite); this.scene.sys.updateList.remove(sprite);
} }
} }
this.list.forEach(objectContaining => objectContaining.destroy()) this.list.forEach((objectContaining) => objectContaining.destroy());
super.destroy(); super.destroy();
} }
isSilent() {
isSilentStore.set(true);
}
noSilent() {
isSilentStore.set(false);
}
playEmote(emoteKey: string) { playEmote(emoteKey: string) {
this.cancelPreviousEmote(); this.cancelPreviousEmote();
@ -270,7 +296,7 @@ export abstract class Character extends Container {
const emoteY = -30 - scalingFactor * 10; const emoteY = -30 - scalingFactor * 10;
this.playerName.setVisible(false); this.playerName.setVisible(false);
this.emote = new Sprite(this.scene, 0, 0, emoteKey); this.emote = new Sprite(this.scene, 0, 0, emoteKey);
this.emote.setAlpha(0); this.emote.setAlpha(0);
this.emote.setScale(0.1 * scalingFactor); this.emote.setScale(0.1 * scalingFactor);
this.add(this.emote); this.add(this.emote);
@ -287,11 +313,11 @@ export abstract class Character extends Container {
alpha: 1, alpha: 1,
y: emoteY, y: emoteY,
}, },
ease: 'Power2', ease: "Power2",
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
this.startPulseTransition(emoteY, scalingFactor); this.startPulseTransition(emoteY, scalingFactor);
} },
}); });
} }
@ -300,7 +326,7 @@ export abstract class Character extends Container {
targets: this.emote, targets: this.emote,
props: { props: {
y: emoteY * 1.3, y: emoteY * 1.3,
scale: scalingFactor * 1.1 scale: scalingFactor * 1.1,
}, },
duration: 250, duration: 250,
yoyo: true, yoyo: true,
@ -308,7 +334,7 @@ export abstract class Character extends Container {
completeDelay: 200, completeDelay: 200,
onComplete: () => { onComplete: () => {
this.startExitTransition(emoteY); this.startExitTransition(emoteY);
} },
}); });
} }
@ -319,11 +345,11 @@ export abstract class Character extends Container {
alpha: 0, alpha: 0,
y: 2 * emoteY, y: 2 * emoteY,
}, },
ease: 'Power2', ease: "Power2",
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
this.destroyEmote(); this.destroyEmote();
} },
}); });
} }
@ -331,7 +357,7 @@ export abstract class Character extends Container {
if (!this.emote) return; if (!this.emote) return;
this.emoteTween?.remove(); this.emoteTween?.remove();
this.destroyEmote() this.destroyEmote();
} }
private destroyEmote() { private destroyEmote() {

View file

@ -1,16 +1,15 @@
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type {PointInterface} from "../../Connexion/ConnexionModels"; import type { PointInterface } from "../../Connexion/ConnexionModels";
import {Character} from "../Entity/Character"; import { Character } from "../Entity/Character";
import type {PlayerAnimationDirections} from "../Player/Animation"; import type { PlayerAnimationDirections } from "../Player/Animation";
import {requestVisitCardsStore} from "../../Stores/GameStore"; import { requestVisitCardsStore } from "../../Stores/GameStore";
/** /**
* Class representing the sprite of a remote player (a player that plays on another computer) * Class representing the sprite of a remote player (a player that plays on another computer)
*/ */
export class RemotePlayer extends Character { export class RemotePlayer extends Character {
userId: number; userId: number;
private visitCardUrl: string|null; private visitCardUrl: string | null;
constructor( constructor(
userId: number, userId: number,
@ -21,19 +20,33 @@ export class RemotePlayer extends Character {
texturesPromise: Promise<string[]>, texturesPromise: Promise<string[]>,
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
visitCardUrl: string|null, visitCardUrl: string | null,
companion: string|null, companion: string | null,
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1, !!visitCardUrl, companion, companionTexturePromise); super(
Scene,
x,
y,
texturesPromise,
name,
direction,
moving,
1,
!!visitCardUrl,
companion,
companionTexturePromise
);
//set data //set data
this.userId = userId; this.userId = userId;
this.visitCardUrl = visitCardUrl; this.visitCardUrl = visitCardUrl;
this.on('pointerdown', () => { this.on("pointerdown", (event: Phaser.Input.Pointer) => {
requestVisitCardsStore.set(this.visitCardUrl); if (event.downElement.nodeName === "CANVAS") {
}) requestVisitCardsStore.set(this.visitCardUrl);
}
});
} }
updatePosition(position: PointInterface): void { updatePosition(position: PointInterface): void {

View file

@ -1,37 +1,36 @@
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import {emoteEventStream} from "../../Connexion/EmoteEventStream"; import { emoteEventStream } from "../../Connexion/EmoteEventStream";
import type {GameScene} from "./GameScene"; import type { GameScene } from "./GameScene";
import type {RadialMenuItem} from "../Components/RadialMenu"; import type { RadialMenuItem } from "../Components/RadialMenu";
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type {Subscription} from "rxjs"; import type { Subscription } from "rxjs";
interface RegisteredEmote extends BodyResourceDescriptionInterface { interface RegisteredEmote extends BodyResourceDescriptionInterface {
name: string; name: string;
img: string; img: string;
} }
export const emotes: {[key: string]: RegisteredEmote} = { export const emotes: { [key: string]: RegisteredEmote } = {
'emote-heart': {name: 'emote-heart', img: 'resources/emotes/heart-emote.png'}, "emote-heart": { name: "emote-heart", img: "resources/emotes/heart-emote.png" },
'emote-clap': {name: 'emote-clap', img: 'resources/emotes/clap-emote.png'}, "emote-clap": { name: "emote-clap", img: "resources/emotes/clap-emote.png" },
'emote-hand': {name: 'emote-hand', img: 'resources/emotes/hand-emote.png'}, "emote-hand": { name: "emote-hand", img: "resources/emotes/hand-emote.png" },
'emote-thanks': {name: 'emote-thanks', img: 'resources/emotes/thanks-emote.png'}, "emote-thanks": { name: "emote-thanks", img: "resources/emotes/thanks-emote.png" },
'emote-thumb-up': {name: 'emote-thumb-up', img: 'resources/emotes/thumb-up-emote.png'}, "emote-thumb-up": { name: "emote-thumb-up", img: "resources/emotes/thumb-up-emote.png" },
'emote-thumb-down': {name: 'emote-thumb-down', img: 'resources/emotes/thumb-down-emote.png'}, "emote-thumb-down": { name: "emote-thumb-down", img: "resources/emotes/thumb-down-emote.png" },
}; };
export class EmoteManager { export class EmoteManager {
private subscription: Subscription; private subscription: Subscription;
constructor(private scene: GameScene) { constructor(private scene: GameScene) {
this.subscription = emoteEventStream.stream.subscribe((event) => { this.subscription = emoteEventStream.stream.subscribe((event) => {
const actor = this.scene.MapPlayersByKey.get(event.userId); const actor = this.scene.MapPlayersByKey.get(event.userId);
if (actor) { if (actor) {
this.lazyLoadEmoteTexture(event.emoteName).then(emoteKey => { this.lazyLoadEmoteTexture(event.emoteName).then((emoteKey) => {
actor.playEmote(emoteKey); actor.playEmote(emoteKey);
}) });
} }
}) });
} }
createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) { createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) {
return new Promise<string>((res) => { return new Promise<string>((res) => {
@ -39,20 +38,22 @@ export class EmoteManager {
return res(playerResourceDescriptor.name); return res(playerResourceDescriptor.name);
} }
loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img); loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img);
loadPlugin.once('filecomplete-image-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor.name)); loadPlugin.once("filecomplete-image-" + playerResourceDescriptor.name, () =>
res(playerResourceDescriptor.name)
);
}); });
} }
lazyLoadEmoteTexture(textureKey: string): Promise<string> { lazyLoadEmoteTexture(textureKey: string): Promise<string> {
const emoteDescriptor = emotes[textureKey]; const emoteDescriptor = emotes[textureKey];
if (emoteDescriptor === undefined) { if (emoteDescriptor === undefined) {
throw 'Emote not found!'; throw "Emote not found!";
} }
const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor); const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor);
this.scene.load.start(); this.scene.load.start();
return loadPromise return loadPromise;
} }
getMenuImages(): Promise<RadialMenuItem[]> { getMenuImages(): Promise<RadialMenuItem[]> {
const promises = []; const promises = [];
for (const key in emotes) { for (const key in emotes) {
@ -60,14 +61,14 @@ export class EmoteManager {
return { return {
image: textureKey, image: textureKey,
name: textureKey, name: textureKey,
} };
}); });
promises.push(promise); promises.push(promise);
} }
return Promise.all(promises); return Promise.all(promises);
} }
destroy() { destroy() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
} }

View file

@ -1,7 +1,6 @@
import { GameScene } from "./GameScene"; import { GameScene } from "./GameScene";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import type { Room } from "../../Connexion/Room"; import type { Room } from "../../Connexion/Room";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import { LoginSceneName } from "../Login/LoginScene"; import { LoginSceneName } from "../Login/LoginScene";
import { SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { EnableCameraSceneName } from "../Login/EnableCameraScene"; import { EnableCameraSceneName } from "../Login/EnableCameraScene";
@ -9,6 +8,7 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore"; import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import { menuIconVisiblilityStore } from "../../Stores/MenuStore";
/** /**
* This class should be responsible for any scene starting/stopping * This class should be responsible for any scene starting/stopping
@ -18,24 +18,34 @@ export class GameManager {
private characterLayers: string[] | null; private characterLayers: string[] | null;
private companion: string | null; private companion: string | null;
private startRoom!: Room; private startRoom!: Room;
private cameraSetup?: { video: unknown; audio: unknown };
currentGameSceneName: string | null = null; currentGameSceneName: string | null = null;
// Note: this scenePlugin is the scenePlugin of the EntryScene. We should always provide a key in methods called on this scenePlugin.
private scenePlugin!: Phaser.Scenes.ScenePlugin;
constructor() { constructor() {
this.playerName = localUserStore.getName(); this.playerName = localUserStore.getName();
this.characterLayers = localUserStore.getCharacterLayers(); this.characterLayers = localUserStore.getCharacterLayers();
this.companion = localUserStore.getCompanion(); this.companion = localUserStore.getCompanion();
this.cameraSetup = localUserStore.getCameraSetup();
} }
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
this.scenePlugin = scenePlugin;
this.startRoom = await connectionManager.initGameConnexion(); this.startRoom = await connectionManager.initGameConnexion();
this.loadMap(this.startRoom, scenePlugin); this.loadMap(this.startRoom);
if (!this.playerName) { //If player name was not set show login scene with player name
//If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous)
if (!this.playerName || (this.startRoom.authenticationMandatory && !localUserStore.getAuthToken())) {
return LoginSceneName; return LoginSceneName;
} else if (!this.characterLayers || !this.characterLayers.length) { } else if (!this.characterLayers || !this.characterLayers.length) {
return SelectCharacterSceneName; return SelectCharacterSceneName;
} else { } else if (this.cameraSetup == undefined) {
return EnableCameraSceneName; return EnableCameraSceneName;
} else {
this.activeMenuSceneAndHelpCameraSettings();
return this.startRoom.key;
} }
} }
@ -68,21 +78,27 @@ export class GameManager {
return this.companion; return this.companion;
} }
public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) { public loadMap(room: Room) {
const roomID = room.key; const roomID = room.key;
const gameIndex = scenePlugin.getIndex(roomID); const gameIndex = this.scenePlugin.getIndex(roomID);
if (gameIndex === -1) { if (gameIndex === -1) {
const game: Phaser.Scene = new GameScene(room, room.mapUrl); const game: Phaser.Scene = new GameScene(room, room.mapUrl);
scenePlugin.add(roomID, game, false); this.scenePlugin.add(roomID, game, false);
} }
} }
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void { public goToStartingMap(): void {
console.log("starting " + (this.currentGameSceneName || this.startRoom.key)); console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
scenePlugin.start(this.currentGameSceneName || this.startRoom.key); this.scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
scenePlugin.launch(MenuSceneName); this.activeMenuSceneAndHelpCameraSettings();
}
/**
* @private
* @return void
*/
private activeMenuSceneAndHelpCameraSettings(): void {
if ( if (
!localUserStore.getHelpCameraSettingsShown() && !localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState)) (!get(requestedMicrophoneState) || !get(requestedCameraState))
@ -94,41 +110,44 @@ export class GameManager {
public gameSceneIsCreated(scene: GameScene) { public gameSceneIsCreated(scene: GameScene) {
this.currentGameSceneName = scene.scene.key; this.currentGameSceneName = scene.scene.key;
const menuScene: MenuScene = scene.scene.get(MenuSceneName) as MenuScene; menuIconVisiblilityStore.set(true);
menuScene.revealMenuIcon();
} }
/** /**
* Temporary leave a gameScene to go back to the loginScene for example. * Temporary leave a gameScene to go back to the loginScene for example.
* This will close the socket connections and stop the gameScene, but won't remove it. * This will close the socket connections and stop the gameScene, but won't remove it.
*/ */
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void { leaveGame(targetSceneName: string, sceneClass: Phaser.Scene): void {
if (this.currentGameSceneName === null) throw "No current scene id set!"; if (this.currentGameSceneName === null) throw "No current scene id set!";
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene; const gameScene: GameScene = this.scenePlugin.get(this.currentGameSceneName) as GameScene;
gameScene.cleanupClosingScene(); gameScene.cleanupClosingScene();
scene.scene.stop(this.currentGameSceneName); gameScene.createSuccessorGameScene(false, false);
scene.scene.sleep(MenuSceneName); menuIconVisiblilityStore.set(false);
if (!scene.scene.get(targetSceneName)) { if (!this.scenePlugin.get(targetSceneName)) {
scene.scene.add(targetSceneName, sceneClass, false); this.scenePlugin.add(targetSceneName, sceneClass, false);
} }
scene.scene.run(targetSceneName); this.scenePlugin.run(targetSceneName);
} }
/** /**
* follow up to leaveGame() * follow up to leaveGame()
*/ */
tryResumingGame(scene: Phaser.Scene, fallbackSceneName: string) { tryResumingGame(fallbackSceneName: string) {
if (this.currentGameSceneName) { if (this.currentGameSceneName) {
scene.scene.start(this.currentGameSceneName); this.scenePlugin.start(this.currentGameSceneName);
scene.scene.wake(MenuSceneName); menuIconVisiblilityStore.set(true);
} else { } else {
scene.scene.run(fallbackSceneName); this.scenePlugin.run(fallbackSceneName);
} }
} }
public getCurrentGameScene(scene: Phaser.Scene): GameScene { public getCurrentGameScene(): GameScene {
if (this.currentGameSceneName === null) throw "No current scene id set!"; if (this.currentGameSceneName === null) throw "No current scene id set!";
return scene.scene.get(this.currentGameSceneName) as GameScene; return this.scenePlugin.get(this.currentGameSceneName) as GameScene;
}
public get currentStartedRoom() {
return this.startRoom;
} }
} }

View file

@ -80,8 +80,11 @@ export class GameMap {
return; return;
} }
this.key = key; this.key = key;
this.triggerAll();
}
const newProps = this.getProperties(key); private triggerAll(): void {
const newProps = this.getProperties(this.key ?? 0);
const oldProps = this.lastProperties; const oldProps = this.lastProperties;
this.lastProperties = newProps; this.lastProperties = newProps;
@ -253,4 +256,34 @@ export class GameMap {
} }
return this.tileNameMap.get(tile); return this.tileNameMap.get(tile);
} }
public setLayerProperty(
layerName: string,
propertyName: string,
propertyValue: string | number | undefined | boolean
) {
const layer = this.findLayer(layerName);
if (layer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (layer.properties === undefined) {
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
this.triggerAll();
}
} }

View file

@ -0,0 +1,55 @@
import type { GameScene } from "./GameScene";
import type { GameMap } from "./GameMap";
import { scriptUtils } from "../../Api/ScriptUtils";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import {
ON_ACTION_TRIGGER_BUTTON,
TRIGGER_WEBSITE_PROPERTIES,
WEBSITE_MESSAGE_PROPERTIES,
} from "../../WebRtc/LayoutManager";
export class GameMapPropertiesListener {
constructor(private scene: GameScene, private gameMap: GameMap) {}
register() {
this.gameMap.onPropertyChange("openTab", (newValue) => {
if (typeof newValue == "string" && newValue.length) {
scriptUtils.openTab(newValue);
}
});
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("openWebsite");
coWebsiteManager.closeCoWebsite();
} else {
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(
newValue as string,
this.scene.MapUrlFile,
allProps.get("openWebsiteAllowApi") as boolean | undefined,
allProps.get("openWebsitePolicy") as string | undefined,
allProps.get("openWebsiteWidth") as number | undefined
);
layoutManagerActionStore.removeAction("openWebsite");
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES);
if (message === undefined) {
message = "Press SPACE or touch here to open web site";
}
layoutManagerActionStore.addAction({
uuid: "openWebsite",
type: "message",
message: message,
callback: () => openWebsiteFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openWebsiteFunction();
}
}
});
}
}

View file

@ -37,7 +37,7 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer"; import { SimplePeer } from "../../WebRtc/SimplePeer";
import { addLoader } from "../Components/Loader"; import { addLoader, removeLoader } from "../Components/Loader";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer"; import { RemotePlayer } from "../Entity/RemotePlayer";
@ -45,7 +45,6 @@ import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import { PlayerAnimationDirections } from "../Player/Animation"; import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene"; import { ErrorSceneName } from "../Reconnecting/ErrorScene";
@ -92,9 +91,9 @@ import { PropertyUtils } from "../Map/PropertyUtils";
import Tileset = Phaser.Tilemaps.Tileset; import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore"; import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { get } from "svelte/store";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore"; import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import type { RadialMenuItem } from "../Components/RadialMenu";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -293,8 +292,11 @@ export class GameScene extends DirtyScene {
} }
//once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading. //once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading.
console.error("Error when loading: ", file);
if (this.preloading) { if (this.preloading) {
//remove loader in progress
removeLoader(this);
//display an error scene
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: "Network error", title: "Network error",
subTitle: "An error occurred while loading resource:", subTitle: "An error occurred while loading resource:",
@ -405,12 +407,6 @@ export class GameScene extends DirtyScene {
}); });
}); });
} }
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
iframeListener.registerScript(script);
}
} }
//hook initialisation //hook initialisation
@ -569,6 +565,12 @@ export class GameScene extends DirtyScene {
} }
this.createPromiseResolve(); this.createPromiseResolve();
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
const scriptPromises = [];
for (const script of scripts) {
scriptPromises.push(iframeListener.registerScript(script));
}
this.userInputManager.spaceEvent(() => { this.userInputManager.spaceEvent(() => {
this.outlinedItem?.activate(); this.outlinedItem?.activate();
@ -583,9 +585,11 @@ export class GameScene extends DirtyScene {
this.updateCameraOffset(box) this.updateCameraOffset(box)
); );
new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange(); this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) { if (!this.room.isDisconnected()) {
this.scene.sleep();
this.connect(); this.connect();
} }
@ -609,6 +613,10 @@ export class GameScene extends DirtyScene {
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => { this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => {
this.openChatIcon.setVisible(!v); this.openChatIcon.setVisible(!v);
}); });
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => {
this.scene.wake();
});
} }
/** /**
@ -686,19 +694,7 @@ export class GameScene extends DirtyScene {
this.connection.onServerDisconnected(() => { this.connection.onServerDisconnected(() => {
console.log("Player disconnected from server. Reloading scene."); console.log("Player disconnected from server. Reloading scene.");
this.cleanupClosingScene(); this.cleanupClosingScene();
this.createSuccessorGameScene(true, true);
const gameSceneKey = "somekey" + Math.round(Math.random() * 10000);
const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile, gameSceneKey);
this.scene.add(gameSceneKey, game, true, {
initPosition: {
x: this.CurrentPlayer.x,
y: this.CurrentPlayer.y,
},
reconnecting: true,
});
this.scene.stop(this.scene.key);
this.scene.remove(this.scene.key);
}); });
this.connection.onActionableEvent((message) => { this.connection.onActionableEvent((message) => {
@ -722,26 +718,9 @@ export class GameScene extends DirtyScene {
}); });
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); this.simplePeer = new SimplePeer(this.connection);
peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(peer) {
//self.openChatIcon.setVisible(true);
audioManagerVolumeStore.setTalking(true);
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
//self.openChatIcon.setVisible(false);
audioManagerVolumeStore.setTalking(false);
}
},
});
//listen event to share position of user //listen event to share position of user
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)); this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this));
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)); this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this));
@ -760,8 +739,9 @@ export class GameScene extends DirtyScene {
this.connectionAnswerPromiseResolve(onConnect.room); this.connectionAnswerPromiseResolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console. // Analyze tags to find if we are admin. If yes, show console.
this.scene.wake(); if (this.scene.isSleeping()) {
this.scene.stop(ReconnectingSceneName); this.scene.stop(ReconnectingSceneName);
}
//init user position and play trigger to check layers properties //init user position and play trigger to check layers properties
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
@ -834,39 +814,7 @@ export class GameScene extends DirtyScene {
}, 2000); }, 2000);
} }
}); });
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("openWebsite");
coWebsiteManager.closeCoWebsite();
} else {
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(
newValue as string,
this.MapUrlFile,
allProps.get("openWebsiteAllowApi") as boolean | undefined,
allProps.get("openWebsitePolicy") as string | undefined
);
layoutManagerActionStore.removeAction("openWebsite");
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES);
if (message === undefined) {
message = "Press SPACE or touch here to open web site";
}
layoutManagerActionStore.addAction({
uuid: "openWebsite",
type: "message",
message: message,
callback: () => openWebsiteFunction(),
userInputManager: this.userInputManager,
});
} else {
openWebsiteFunction();
}
}
});
this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi"); layoutManagerActionStore.removeAction("jitsi");
@ -906,8 +854,10 @@ export class GameScene extends DirtyScene {
this.gameMap.onPropertyChange("silent", (newValue, oldValue) => { this.gameMap.onPropertyChange("silent", (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") { if (newValue === undefined || newValue === false || newValue === "") {
this.connection?.setSilent(false); this.connection?.setSilent(false);
this.CurrentPlayer.noSilent();
} else { } else {
this.connection?.setSilent(true); this.connection?.setSilent(true);
this.CurrentPlayer.isSilent();
} }
}); });
this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => {
@ -927,9 +877,10 @@ export class GameScene extends DirtyScene {
}); });
this.gameMap.onPropertyChange("zone", (newValue, oldValue) => { this.gameMap.onPropertyChange("zone", (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") { if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string); iframeListener.sendLeaveEvent(oldValue as string);
} else { }
if (newValue) {
iframeListener.sendEnterEvent(newValue as string); iframeListener.sendEnterEvent(newValue as string);
} }
}); });
@ -952,9 +903,13 @@ export class GameScene extends DirtyScene {
return; return;
} }
const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message); const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message);
let html = `<div id="container" hidden><div class="nes-container with-title is-centered"> let html = '<div id="container" hidden>';
if (escapedMessage) {
html += `<div class="nes-container with-title is-centered">
${escapedMessage} ${escapedMessage}
</div> `; </div> `;
}
const buttonContainer = '<div class="buttonContainer"</div>'; const buttonContainer = '<div class="buttonContainer"</div>';
html += buttonContainer; html += buttonContainer;
let id = 0; let id = 0;
@ -984,7 +939,11 @@ ${escapedMessage}
const btnId = id; const btnId = id;
button.onclick = () => { button.onclick = () => {
iframeListener.sendButtonClickedEvent(openPopupEvent.popupId, btnId); iframeListener.sendButtonClickedEvent(openPopupEvent.popupId, btnId);
// Disable for a short amount of time to let time to the script to remove the popup
button.disabled = true; button.disabled = true;
setTimeout(() => {
button.disabled = false;
}, 100);
}; };
id++; id++;
} }
@ -1240,30 +1199,10 @@ ${escapedMessage}
propertyName: string, propertyName: string,
propertyValue: string | number | boolean | undefined propertyValue: string | number | boolean | undefined
): void { ): void {
const layer = this.gameMap.findLayer(layerName);
if (layer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (propertyName === "exitUrl" && typeof propertyValue === "string") { if (propertyName === "exitUrl" && typeof propertyValue === "string") {
this.loadNextGameFromExitUrl(propertyValue); this.loadNextGameFromExitUrl(propertyValue);
} }
if (layer.properties === undefined) { this.gameMap.setLayerProperty(layerName, propertyName, propertyValue);
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
} }
private setLayerVisibility(layerName: string, visible: boolean): void { private setLayerVisibility(layerName: string, visible: boolean): void {
@ -1322,13 +1261,12 @@ ${escapedMessage}
urlManager.pushStartLayerNameToUrl(roomUrl.hash); urlManager.pushStartLayerNameToUrl(roomUrl.hash);
} }
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
menuScene.reset();
if (!targetRoom.isEqual(this.room)) { if (!targetRoom.isEqual(this.room)) {
if (this.scene.get(targetRoom.key) === null) { if (this.scene.get(targetRoom.key) === null) {
console.error("next room not loaded", targetRoom.key); console.error("next room not loaded", targetRoom.key);
return; // Try to load next dame room from exit URL
// The policy of room can to be updated during a session and not load before
await this.loadNextGameFromExitUrl(targetRoom.key);
} }
this.cleanupClosingScene(); this.cleanupClosingScene();
this.scene.stop(); this.scene.stop();
@ -1368,7 +1306,6 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("getState"); iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset"); iframeListener.unregisterAnswerer("loadTileset");
iframeListener.unregisterAnswerer("getMapData"); iframeListener.unregisterAnswerer("getMapData");
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("triggerActionMessage"); iframeListener.unregisterAnswerer("triggerActionMessage");
iframeListener.unregisterAnswerer("removeActionMessage"); iframeListener.unregisterAnswerer("removeActionMessage");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
@ -1443,7 +1380,7 @@ ${escapedMessage}
private async loadNextGame(exitRoomPath: URL): Promise<void> { private async loadNextGame(exitRoomPath: URL): Promise<void> {
try { try {
const room = await Room.createRoom(exitRoomPath); const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene); return gameManager.loadMap(room);
} catch (e /*: unknown*/) { } catch (e /*: unknown*/) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e); console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
} }
@ -1504,7 +1441,7 @@ ${escapedMessage}
}); });
} catch (err) { } catch (err) {
if (err instanceof TextureError) { if (err instanceof TextureError) {
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
} }
throw err; throw err;
} }
@ -1863,8 +1800,9 @@ ${escapedMessage}
"jitsiInterfaceConfig" "jitsiInterfaceConfig"
); );
const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; const jitsiUrl = allProps.get("jitsiUrl") as string | undefined;
const jitsiWidth = allProps.get("jitsiWidth") as number | undefined;
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl); jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth);
this.connection?.setSilent(true); this.connection?.setSilent(true);
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
@ -1920,4 +1858,24 @@ ${escapedMessage}
waScaleManager.zoomModifier *= zoomFactor; waScaleManager.zoomModifier *= zoomFactor;
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
} }
public createSuccessorGameScene(autostart: boolean, reconnecting: boolean) {
const gameSceneKey = "somekey" + Math.round(Math.random() * 10000);
const game = new GameScene(this.room, this.MapUrlFile, gameSceneKey);
this.scene.add(gameSceneKey, game, autostart, {
initPosition: {
x: this.CurrentPlayer.x,
y: this.CurrentPlayer.y,
},
reconnecting: reconnecting,
});
//If new gameScene doesn't start automatically then we change the gameScene in gameManager so that it can start the new gameScene
if (!autostart) {
gameManager.gameSceneIsCreated(game);
}
this.scene.stop(this.scene.key);
this.scene.remove(this.scene.key);
}
} }

View file

@ -1,10 +1,7 @@
import type { RoomConnection } from "../../Connexion/RoomConnection"; import type { RoomConnection } from "../../Connexion/RoomConnection";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import type { ITile, ITiledMapObject } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap";
import type { Var } from "svelte/types/compiler/interfaces";
import { init } from "svelte/internal";
interface Variable { interface Variable {
defaultValue: unknown; defaultValue: unknown;
@ -100,24 +97,33 @@ export class SharedVariablesManager {
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> { private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
const objects = new Map<string, Variable>(); const objects = new Map<string, Variable>();
for (const layer of gameMap.getMap().layers) { for (const layer of gameMap.getMap().layers) {
if (layer.type === "objectgroup") { this.recursiveFindVariablesInLayer(layer, objects);
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.'
);
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
} }
return 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 { private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = { const variable: Variable = {
defaultValue: undefined, defaultValue: undefined,

View file

@ -282,7 +282,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.scene.sleep(CustomizeSceneName); this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom(); waScaleManager.restoreZoom();
this.events.removeListener("wake"); this.events.removeListener("wake");
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(EnableCameraSceneName);
customCharacterSceneVisibleStore.set(false); customCharacterSceneVisibleStore.set(false);
} }

View file

@ -1,50 +1,35 @@
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {TextField} from "../Components/TextField"; import { ResizableScene } from "./ResizableScene";
import Image = Phaser.GameObjects.Image; import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import {mediaManager} from "../../WebRtc/MediaManager"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {SoundMeter} from "../Components/SoundMeter";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import Zone = Phaser.GameObjects.Zone;
import { MenuScene } from "../Menu/MenuScene";
import {ResizableScene} from "./ResizableScene";
import {
enableCameraSceneVisibilityStore,
} from "../../Stores/MediaStore";
export const EnableCameraSceneName = "EnableCameraScene"; export const EnableCameraSceneName = "EnableCameraScene";
export class EnableCameraScene extends ResizableScene { export class EnableCameraScene extends ResizableScene {
constructor() { constructor() {
super({ super({
key: EnableCameraSceneName key: EnableCameraSceneName,
}); });
} }
preload() { preload() {}
}
create() { create() {
this.input.keyboard.on("keyup-ENTER", () => {
this.input.keyboard.on('keyup-ENTER', () => {
this.login(); this.login();
}); });
enableCameraSceneVisibilityStore.showEnableCameraScene(); enableCameraSceneVisibilityStore.showEnableCameraScene();
} }
public onResize(): void { public onResize(): void {}
}
update(time: number, delta: number): void { update(time: number, delta: number): void {}
}
public login(): void { public login(): void {
enableCameraSceneVisibilityStore.hideEnableCameraScene(); enableCameraSceneVisibilityStore.hideEnableCameraScene();
this.scene.sleep(EnableCameraSceneName); this.scene.sleep(EnableCameraSceneName);
gameManager.goToStartingMap(this.scene); gameManager.goToStartingMap();
} }
} }

View file

@ -1,25 +1,35 @@
import {gameManager} from "../Game/GameManager"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
import {SelectCharacterSceneName} from "./SelectCharacterScene"; import { ResizableScene } from "./ResizableScene";
import {ResizableScene} from "./ResizableScene"; import { loginSceneVisibleIframeStore, loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { gameManager } from "../Game/GameManager";
export const LoginSceneName = "LoginScene"; export const LoginSceneName = "LoginScene";
export class LoginScene extends ResizableScene { export class LoginScene extends ResizableScene {
private name: string = "";
private name: string = '';
constructor() { constructor() {
super({ super({
key: LoginSceneName key: LoginSceneName,
}); });
this.name = gameManager.getPlayerName() || ''; this.name = gameManager.getPlayerName() || "";
} }
preload() { preload() {}
}
create() { create() {
loginSceneVisibleIframeStore.set(false);
//If authentication is mandatory, push authentication iframe
if (
localUserStore.getAuthToken() == undefined &&
gameManager.currentStartedRoom &&
gameManager.currentStartedRoom?.authenticationMandatory
) {
connectionManager.loadOpenIDScreen();
loginSceneVisibleIframeStore.set(true);
}
loginSceneVisibleStore.set(true); loginSceneVisibleStore.set(true);
} }
@ -27,15 +37,13 @@ export class LoginScene extends ResizableScene {
name = name.trim(); name = name.trim();
gameManager.setPlayerName(name); gameManager.setPlayerName(name);
this.scene.stop(LoginSceneName) this.scene.stop(LoginSceneName);
gameManager.tryResumingGame(this, SelectCharacterSceneName); gameManager.tryResumingGame(SelectCharacterSceneName);
this.scene.remove(LoginSceneName); this.scene.remove(LoginSceneName);
loginSceneVisibleStore.set(false); loginSceneVisibleStore.set(false);
} }
update(time: number, delta: number): void { update(time: number, delta: number): void {}
}
public onResize(): void { public onResize(): void {}
}
} }

View file

@ -101,7 +101,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.scene.stop(SelectCharacterSceneName); this.scene.stop(SelectCharacterSceneName);
waScaleManager.restoreZoom(); waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]); gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(EnableCameraSceneName);
this.players = []; this.players = [];
selectCharacterSceneVisibleStore.set(false); selectCharacterSceneVisibleStore.set(false);
this.events.removeListener("wake"); this.events.removeListener("wake");

View file

@ -1,18 +1,15 @@
import Image = Phaser.GameObjects.Image;
import Rectangle = Phaser.GameObjects.Rectangle;
import { addLoader } from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import { gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene"; import { ResizableScene } from "./ResizableScene";
import { EnableCameraSceneName } from "./EnableCameraScene"; import { EnableCameraSceneName } from "./EnableCameraScene";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import type { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures"; import type { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures";
import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingManager"; import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingManager";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { MenuScene } from "../Menu/MenuScene"; import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore"; import { waScaleManager } from "../Services/WaScaleManager";
import {waScaleManager} from "../Services/WaScaleManager"; import { isMobile } from "../../Enum/EnvironmentVariable";
import {isMobile} from "../../Enum/EnvironmentVariable";
export const SelectCompanionSceneName = "SelectCompanionScene"; export const SelectCompanionSceneName = "SelectCompanionScene";
@ -28,12 +25,12 @@ export class SelectCompanionScene extends ResizableScene {
constructor() { constructor() {
super({ super({
key: SelectCompanionSceneName key: SelectCompanionSceneName,
}); });
} }
preload() { preload() {
getAllCompanionResources(this.load).forEach(model => { getAllCompanionResources(this.load).forEach((model) => {
this.companionModels.push(model); this.companionModels.push(model);
}); });
@ -42,7 +39,6 @@ export class SelectCompanionScene extends ResizableScene {
} }
create() { create() {
selectCompanionSceneVisibleStore.set(true); selectCompanionSceneVisibleStore.set(true);
waScaleManager.saveZoom(); waScaleManager.saveZoom();
@ -53,14 +49,16 @@ export class SelectCompanionScene extends ResizableScene {
} }
// input events // input events
this.input.keyboard.on('keyup-ENTER', this.selectCompanion.bind(this)); this.input.keyboard.on("keyup-ENTER", this.selectCompanion.bind(this));
this.input.keyboard.on('keydown-RIGHT', this.moveToRight.bind(this)); this.input.keyboard.on("keydown-RIGHT", this.moveToRight.bind(this));
this.input.keyboard.on('keydown-LEFT', this.moveToLeft.bind(this)); this.input.keyboard.on("keydown-LEFT", this.moveToLeft.bind(this));
if(localUserStore.getCompanion()){ if (localUserStore.getCompanion()) {
const companionIndex = this.companionModels.findIndex((companion) => companion.name === localUserStore.getCompanion()); const companionIndex = this.companionModels.findIndex(
if(companionIndex > -1 || companionIndex < this.companions.length){ (companion) => companion.name === localUserStore.getCompanion()
);
if (companionIndex > -1 || companionIndex < this.companions.length) {
this.currentCompanion = companionIndex; this.currentCompanion = companionIndex;
this.selectedCompanion = this.companions[companionIndex]; this.selectedCompanion = this.companions[companionIndex];
} }
@ -89,26 +87,26 @@ export class SelectCompanionScene extends ResizableScene {
this.closeScene(); this.closeScene();
} }
public closeScene(){ public closeScene() {
// next scene // next scene
this.scene.stop(SelectCompanionSceneName); this.scene.stop(SelectCompanionSceneName);
waScaleManager.restoreZoom(); waScaleManager.restoreZoom();
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(EnableCameraSceneName);
this.scene.remove(SelectCompanionSceneName); this.scene.remove(SelectCompanionSceneName);
selectCompanionSceneVisibleStore.set(false); selectCompanionSceneVisibleStore.set(false);
} }
private createCurrentCompanion(): void { private createCurrentCompanion(): void {
for (let i = 0; i < this.companionModels.length; i++) { for (let i = 0; i < this.companionModels.length; i++) {
const companionResource = this.companionModels[i] const companionResource = this.companionModels[i];
const [middleX, middleY] = this.getCompanionPosition(); const [middleX, middleY] = this.getCompanionPosition();
const companion = this.physics.add.sprite(middleX, middleY, companionResource.name, 0); const companion = this.physics.add.sprite(middleX, middleY, companionResource.name, 0);
this.setUpCompanion(companion, i); this.setUpCompanion(companion, i);
this.anims.create({ this.anims.create({
key: companionResource.name, key: companionResource.name,
frames: this.anims.generateFrameNumbers(companionResource.name, {start: 0, end: 2,}), frames: this.anims.generateFrameNumbers(companionResource.name, { start: 0, end: 2 }),
frameRate: 10, frameRate: 10,
repeat: -1 repeat: -1,
}); });
companion.setInteractive().on("pointerdown", () => { companion.setInteractive().on("pointerdown", () => {
@ -140,87 +138,84 @@ export class SelectCompanionScene extends ResizableScene {
this.selectedCompanion = companion; this.selectedCompanion = companion;
} }
private moveCompanion(){ private moveCompanion() {
for(let i = 0; i < this.companions.length; i++){ for (let i = 0; i < this.companions.length; i++) {
const companion = this.companions[i]; const companion = this.companions[i];
this.setUpCompanion(companion, i); this.setUpCompanion(companion, i);
} }
this.updateSelectedCompanion(); this.updateSelectedCompanion();
} }
public moveToRight(){ public moveToRight() {
if(this.currentCompanion === (this.companions.length - 1)){ if (this.currentCompanion === this.companions.length - 1) {
return; return;
} }
this.currentCompanion += 1; this.currentCompanion += 1;
this.moveCompanion(); this.moveCompanion();
} }
public moveToLeft(){ public moveToLeft() {
if(this.currentCompanion === 0){ if (this.currentCompanion === 0) {
return; return;
} }
this.currentCompanion -= 1; this.currentCompanion -= 1;
this.moveCompanion(); this.moveCompanion();
} }
private defineSetupCompanion(num: number){ private defineSetupCompanion(num: number) {
const deltaX = 30; const deltaX = 30;
const deltaY = 2; const deltaY = 2;
let [companionX, companionY] = this.getCompanionPosition(); let [companionX, companionY] = this.getCompanionPosition();
let companionVisible = true; let companionVisible = true;
let companionScale = 1.5; let companionScale = 1.5;
let companionOpactity = 1; let companionOpactity = 1;
if( this.currentCompanion !== num ){ if (this.currentCompanion !== num) {
companionVisible = false; companionVisible = false;
} }
if( num === (this.currentCompanion + 1) ){ if (num === this.currentCompanion + 1) {
companionY -= deltaY; companionY -= deltaY;
companionX += deltaX; companionX += deltaX;
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
if( num === (this.currentCompanion + 2) ){ if (num === this.currentCompanion + 2) {
companionY -= deltaY; companionY -= deltaY;
companionX += (deltaX * 2); companionX += deltaX * 2;
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
if( num === (this.currentCompanion - 1) ){ if (num === this.currentCompanion - 1) {
companionY -= deltaY; companionY -= deltaY;
companionX -= deltaX; companionX -= deltaX;
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
if( num === (this.currentCompanion - 2) ){ if (num === this.currentCompanion - 2) {
companionY -= deltaY; companionY -= deltaY;
companionX -= (deltaX * 2); companionX -= deltaX * 2;
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
return {companionX, companionY, companionScale, companionOpactity, companionVisible} return { companionX, companionY, companionScale, companionOpactity, companionVisible };
} }
/** /**
* Returns pixel position by on column and row number * Returns pixel position by on column and row number
*/ */
private getCompanionPosition(): [number, number] { private getCompanionPosition(): [number, number] {
return [ return [this.game.renderer.width / 2, this.game.renderer.height / 3];
this.game.renderer.width / 2,
this.game.renderer.height / 3
];
} }
private setUpCompanion(companion: Phaser.Physics.Arcade.Sprite, numero: number){ private setUpCompanion(companion: Phaser.Physics.Arcade.Sprite, numero: number) {
const { companionX, companionY, companionScale, companionOpactity, companionVisible } =
const {companionX, companionY, companionScale, companionOpactity, companionVisible} = this.defineSetupCompanion(numero); this.defineSetupCompanion(numero);
companion.setBounce(0.2); companion.setBounce(0.2);
companion.setCollideWorldBounds(true); companion.setCollideWorldBounds(true);
companion.setVisible( companionVisible ); companion.setVisible(companionVisible);
companion.setScale(companionScale, companionScale); companion.setScale(companionScale, companionScale);
companion.setAlpha(companionOpactity); companion.setAlpha(companionOpactity);
companion.setX(companionX); companion.setX(companionX);

View file

@ -1,430 +0,0 @@
import { LoginScene, LoginSceneName } from "../Login/LoginScene";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene";
import { gameManager } from "../Game/GameManager";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager";
import { menuIconVisible } from "../../Stores/MenuStore";
import { videoConstraintStore } from "../../Stores/MediaStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { iframeListener } from "../../Api/IframeListener";
import { Subscription } from "rxjs";
import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterEvent";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { get } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore";
import { mediaManager } from "../../WebRtc/MediaManager";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
export const MenuSceneName = "MenuScene";
const gameMenuKey = "gameMenu";
const gameMenuIconKey = "gameMenuIcon";
const gameSettingsMenuKey = "gameSettingsMenu";
const gameShare = "gameShare";
const closedSideMenuX = -1000;
const openedSideMenuX = 0;
/**
* The scene that manages the game menu, rendered using a DOM element.
*/
export class MenuScene extends Phaser.Scene {
private menuElement!: Phaser.GameObjects.DOMElement;
private gameQualityMenuElement!: Phaser.GameObjects.DOMElement;
private gameShareElement!: Phaser.GameObjects.DOMElement;
private gameReportElement!: ReportMenu;
private sideMenuOpened = false;
private settingsMenuOpened = false;
private gameShareOpened = false;
private gameQualityValue: number;
private videoQualityValue: number;
private menuButton!: Phaser.GameObjects.DOMElement;
private subscriptions = new Subscription();
constructor() {
super({ key: MenuSceneName });
this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(
registerMenuCommandStream.subscribe((menuCommand) => {
this.addMenuOption(menuCommand);
})
);
this.subscriptions.add(
iframeListener.unregisterMenuCommandStream.subscribe((menuCommand) => {
this.destroyMenu(menuCommand);
})
);
}
reset() {
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
addedMenuItems[index].remove();
}
}
public addMenuOption(menuText: string) {
const wrappingSection = document.createElement("section");
const escapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`;
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
if (menuItemContainer) {
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove();
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"));
}
}
preload() {
this.load.html(gameMenuKey, "resources/html/gameMenu.html");
this.load.html(gameMenuIconKey, "resources/html/gameMenuIcon.html");
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource);
}
create() {
menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
if (mediaManager.hasNotification()) {
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
}
const middleX = window.innerWidth / 3 - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener("click");
this.gameShareElement.on("click", (event: MouseEvent) => {
event.preventDefault();
if ((event?.target as HTMLInputElement).id === "gameShareFormSubmit") {
this.copyLink();
} else if ((event?.target as HTMLInputElement).id === "gameShareFormCancel") {
this.closeGameShare();
}
});
this.gameReportElement = new ReportMenu(
this,
connectionManager.getConnexionType === GameConnexionTypes.anonymous
);
showReportScreenStore.subscribe((user) => {
if (user !== null) {
this.closeAll();
const uuid = playersStore.getPlayerById(user.userId)?.userUuid;
if (uuid === undefined) {
throw new Error("Could not find UUID for user with ID " + user.userId);
}
this.gameReportElement.open(uuid, user.userName);
}
});
this.input.keyboard.on("keyup-TAB", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
});
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
this.menuButton.addListener("click");
this.menuButton.on("click", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
});
this.menuElement.addListener("click");
this.menuElement.on("click", this.onMenuClick.bind(this));
chatVisibilityStore.subscribe((v) => {
this.menuButton.setVisible(!v);
});
}
//todo put this method in a parent menuElement class
static revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
//Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect.
//To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
setTimeout(() => {
(menuElement.getChildByID(rootDomId) as HTMLElement).hidden = false;
}, 250);
}
public revealMenuIcon(): void {
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
try {
(this.menuButton.getChildByID("menuIcon") as HTMLElement).hidden = false;
} catch (err) {
console.error(err);
}
}
openSideMenu() {
if (this.sideMenuOpened) return;
this.closeAll();
this.sideMenuOpened = true;
this.menuButton.getChildByID("openMenuButton").innerHTML = "X";
const connection = gameManager.getCurrentGameScene(this).connection;
if (connection && connection.isAdmin()) {
const adminSection = this.menuElement.getChildByID("adminConsoleSection") as HTMLElement;
adminSection.hidden = false;
}
//TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID("socialLinks") as HTMLElement;
adminSection.hidden = false;
//}
this.tweens.add({
targets: this.menuElement,
x: openedSideMenuX,
duration: 500,
ease: "Power3",
});
}
private closeSideMenu(): void {
if (!this.sideMenuOpened) return;
this.sideMenuOpened = false;
this.closeAll();
this.menuButton.getChildByID("openMenuButton").innerHTML = `<img src="/static/images/menu.svg">`;
consoleGlobalMessageManagerVisibleStore.set(false);
this.tweens.add({
targets: this.menuElement,
x: closedSideMenuX,
duration: 500,
ease: "Power3",
});
}
private openGameSettingsMenu(): void {
if (this.settingsMenuOpened) {
this.closeGameQualityMenu();
return;
}
//close all
this.closeAll();
this.settingsMenuOpened = true;
const gameQualitySelect = this.gameQualityMenuElement.getChildByID("select-game-quality") as HTMLInputElement;
gameQualitySelect.value = "" + this.gameQualityValue;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID("select-video-quality") as HTMLInputElement;
videoQualitySelect.value = "" + this.videoQualityValue;
this.gameQualityMenuElement.addListener("click");
this.gameQualityMenuElement.on("click", (event: MouseEvent) => {
event.preventDefault();
if ((event?.target as HTMLInputElement).id === "gameQualityFormSubmit") {
const gameQualitySelect = this.gameQualityMenuElement.getChildByID(
"select-game-quality"
) as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID(
"select-video-quality"
) as HTMLInputElement;
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
} else if ((event?.target as HTMLInputElement).id === "gameQualityFormCancel") {
this.closeGameQualityMenu();
}
});
let middleY = this.scale.height / 2 - 392 / 2;
if (middleY < 0) {
middleY = 0;
}
let middleX = this.scale.width / 2 - 457 / 2;
if (middleX < 0) {
middleX = 0;
}
this.tweens.add({
targets: this.gameQualityMenuElement,
y: middleY,
x: middleX,
duration: 1000,
ease: "Power3",
});
}
private closeGameQualityMenu(): void {
if (!this.settingsMenuOpened) return;
this.settingsMenuOpened = false;
this.gameQualityMenuElement.removeListener("click");
this.tweens.add({
targets: this.gameQualityMenuElement,
y: -400,
duration: 1000,
ease: "Power3",
});
}
private openGameShare(): void {
if (this.gameShareOpened) {
this.closeGameShare();
return;
}
//close all
this.closeAll();
const gameShareLink = this.gameShareElement.getChildByID("gameShareLink") as HTMLInputElement;
gameShareLink.value = location.toString();
this.gameShareOpened = true;
let middleY = this.scale.height / 2 - 85;
if (middleY < 0) {
middleY = 0;
}
let middleX = this.scale.width / 2 - 200;
if (middleX < 0) {
middleX = 0;
}
this.tweens.add({
targets: this.gameShareElement,
y: middleY,
x: middleX,
duration: 1000,
ease: "Power3",
});
}
private closeGameShare(): void {
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = "";
gameShareInfo.style.display = "none";
this.gameShareOpened = false;
this.tweens.add({
targets: this.gameShareElement,
y: -400,
duration: 1000,
ease: "Power3",
});
}
private onMenuClick(event: MouseEvent) {
const htmlMenuItem = event?.target as HTMLInputElement;
if (htmlMenuItem.classList.contains("not-button")) {
return;
}
event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) {
sendMenuClickedEvent(htmlMenuItem.id);
return;
}
switch ((event?.target as HTMLInputElement).id) {
case "changeNameButton":
this.closeSideMenu();
gameManager.leaveGame(this, LoginSceneName, new LoginScene());
break;
case "sparkButton":
this.gotToCreateMapPage();
break;
case "changeSkinButton":
this.closeSideMenu();
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
break;
case "changeCompanionButton":
this.closeSideMenu();
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
break;
case "closeButton":
this.closeSideMenu();
break;
case "shareButton":
this.openGameShare();
break;
case "editGameSettingsButton":
this.openGameSettingsMenu();
break;
case "oidcLogin":
connectionManager.loadOpenIDScreen();
break;
case "toggleFullscreen":
this.toggleFullscreen();
break;
case "enableNotification":
this.enableNotification();
break;
case "adminConsoleButton":
if (get(consoleGlobalMessageManagerVisibleStore)) {
consoleGlobalMessageManagerVisibleStore.set(false);
} else {
consoleGlobalMessageManagerVisibleStore.set(true);
}
break;
}
}
private async copyLink() {
await navigator.clipboard.writeText(location.toString());
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = "Link copied, you can share it now!";
gameShareInfo.style.display = "block";
}
private saveSetting(valueGame: number, valueVideo: number) {
if (valueGame !== this.gameQualityValue) {
this.gameQualityValue = valueGame;
localUserStore.setGameQualityValue(valueGame);
window.location.reload();
}
if (valueVideo !== this.videoQualityValue) {
this.videoQualityValue = valueVideo;
localUserStore.setVideoQualityValue(valueVideo);
videoConstraintStore.setFrameRate(valueVideo);
}
this.closeGameQualityMenu();
}
private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO.
//const sparkHost = ADMIN_URL + "/getting-started";
//The redirection must be only on workadventu.re domain
//To day the domain staging cannot be use by customer
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
}
private closeAll() {
this.closeGameQualityMenu();
this.closeGameShare();
this.gameReportElement.close();
}
private toggleFullscreen() {
const body = document.querySelector("body");
if (body) {
if (document.fullscreenElement ?? document.fullscreen) {
document.exitFullscreen();
} else {
body.requestFullscreen();
}
}
}
public destroyMenu(menu: string) {
this.menuElement.getChildByID(menu).remove();
}
public isDirty(): boolean {
return false;
}
private enableNotification() {
mediaManager.requestNotification().then(() => {
if (mediaManager.hasNotification()) {
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
}
});
}
}

View file

@ -1,120 +0,0 @@
import { MenuScene } from "./MenuScene";
import { gameManager } from "../Game/GameManager";
import { blackListManager } from "../../WebRtc/BlackListManager";
import { playersStore } from "../../Stores/PlayersStore";
export const gameReportKey = "gameReport";
export const gameReportRessource = "resources/html/gameReport.html";
export class ReportMenu extends Phaser.GameObjects.DOMElement {
private opened: boolean = false;
private userUuid!: string;
private userName!: string | undefined;
private anonymous: boolean;
constructor(scene: Phaser.Scene, anonymous: boolean) {
super(scene, -2000, -2000);
this.anonymous = anonymous;
this.createFromCache(gameReportKey);
if (this.anonymous) {
const divToHide = this.getChildByID("reportSection") as HTMLElement;
divToHide.hidden = true;
const textToHide = this.getChildByID("askActionP") as HTMLElement;
textToHide.hidden = true;
}
scene.add.existing(this);
MenuScene.revealMenusAfterInit(this, gameReportKey);
this.addListener("click");
this.on("click", (event: MouseEvent) => {
event.preventDefault();
if ((event?.target as HTMLInputElement).id === "gameReportFormSubmit") {
this.submitReport();
} else if ((event?.target as HTMLInputElement).id === "gameReportFormCancel") {
this.close();
} else if ((event?.target as HTMLInputElement).id === "toggleBlockButton") {
this.toggleBlock();
}
});
}
public open(userUuid: string, userName: string | undefined): void {
if (this.opened) {
this.close();
return;
}
this.userUuid = userUuid;
this.userName = userName;
const mainEl = this.getChildByID("gameReport") as HTMLElement;
this.x = this.getCenteredX(mainEl);
this.y = this.getHiddenY(mainEl);
const gameTitleReport = this.getChildByID("nameReported") as HTMLElement;
gameTitleReport.innerText = userName || "";
const blockButton = this.getChildByID("toggleBlockButton") as HTMLElement;
blockButton.innerText = blackListManager.isBlackListed(this.userUuid) ? "Unblock this user" : "Block this user";
this.opened = true;
gameManager.getCurrentGameScene(this.scene).userInputManager.disableControls();
this.scene.tweens.add({
targets: this,
y: this.getCenteredY(mainEl),
duration: 1000,
ease: "Power3",
});
}
public close(): void {
gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls();
this.opened = false;
const mainEl = this.getChildByID("gameReport") as HTMLElement;
this.scene.tweens.add({
targets: this,
y: this.getHiddenY(mainEl),
duration: 1000,
ease: "Power3",
});
}
//todo: into a parent class?
private getCenteredX(mainEl: HTMLElement): number {
return window.innerWidth / 4 - mainEl.clientWidth / 2;
}
private getHiddenY(mainEl: HTMLElement): number {
return -mainEl.clientHeight - 50;
}
private getCenteredY(mainEl: HTMLElement): number {
return window.innerHeight / 4 - mainEl.clientHeight / 2;
}
private toggleBlock(): void {
!blackListManager.isBlackListed(this.userUuid)
? blackListManager.blackList(this.userUuid)
: blackListManager.cancelBlackList(this.userUuid);
this.close();
}
private submitReport(): void {
const gamePError = this.getChildByID("gameReportErr") as HTMLParagraphElement;
gamePError.innerText = "";
gamePError.style.display = "none";
const gameTextArea = this.getChildByID("gameReportInput") as HTMLInputElement;
if (!gameTextArea || !gameTextArea.value) {
gamePError.innerText = "Report message cannot to be empty.";
gamePError.style.display = "block";
return;
}
gameManager
.getCurrentGameScene(this.scene)
.connection?.emitReportPlayerMessage(this.userUuid, gameTextArea.value);
this.close();
}
}

View file

@ -1,9 +1,9 @@
import {PlayerAnimationDirections} from "./Animation"; import { PlayerAnimationDirections } from "./Animation";
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character"; import { Character } from "../Entity/Character";
import {userMovingStore} from "../../Stores/GameStore"; import { userMovingStore } from "../../Stores/GameStore";
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu"; import { RadialMenu, RadialMenuClickEvent, RadialMenuItem } from "../Components/RadialMenu";
export const hasMovedEventName = "hasMoved"; export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote"; export const requestEmoteEventName = "requestEmote";
@ -11,7 +11,7 @@ export const requestEmoteEventName = "requestEmote";
export class Player extends Character { export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down; private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false; private wasMoving: boolean = false;
private emoteMenu: RadialMenu|null = null; private emoteMenu: RadialMenu | null = null;
private updateListener: () => void; private updateListener: () => void;
constructor( constructor(
@ -23,7 +23,7 @@ export class Player extends Character {
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
private userInputManager: UserInputManager, private userInputManager: UserInputManager,
companion: string|null, companion: string | null,
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise); super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
@ -37,7 +37,7 @@ export class Player extends Character {
this.emoteMenu.y = this.y; this.emoteMenu.y = this.y;
} }
}; };
this.scene.events.addListener('postupdate', this.updateListener); this.scene.events.addListener("postupdate", this.updateListener);
} }
moveUser(delta: number): void { moveUser(delta: number): void {
@ -73,14 +73,14 @@ export class Player extends Character {
if (x !== 0 || y !== 0) { if (x !== 0 || y !== 0) {
this.move(x, y); this.move(x, y);
this.emit(hasMovedEventName, {moving, direction, x: this.x, y: this.y}); this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y });
} else if (this.wasMoving && moving) { } else if (this.wasMoving && moving) {
// slow joystick movement // slow joystick movement
this.move(0, 0); this.move(0, 0);
this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
} else if (this.wasMoving && !moving) { } else if (this.wasMoving && !moving) {
this.stop(); this.stop();
this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
} }
if (direction !== null) { if (direction !== null) {
@ -94,17 +94,17 @@ export class Player extends Character {
return this.wasMoving; return this.wasMoving;
} }
openOrCloseEmoteMenu(emotes:RadialMenuItem[]) { openOrCloseEmoteMenu(emotes: RadialMenuItem[]) {
if(this.emoteMenu) { if (this.emoteMenu) {
this.closeEmoteMenu(); this.closeEmoteMenu();
} else { } else {
this.openEmoteMenu(emotes); this.openEmoteMenu(emotes);
} }
} }
openEmoteMenu(emotes:RadialMenuItem[]): void { openEmoteMenu(emotes: RadialMenuItem[]): void {
this.cancelPreviousEmote(); this.cancelPreviousEmote();
this.emoteMenu = new RadialMenu(this.scene, this.x, this.y, emotes) this.emoteMenu = new RadialMenu(this.scene, this.x, this.y, emotes);
this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => { this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => {
this.closeEmoteMenu(); this.closeEmoteMenu();
this.emit(requestEmoteEventName, item.name); this.emit(requestEmoteEventName, item.name);
@ -112,6 +112,13 @@ export class Player extends Character {
}); });
} }
isSilent() {
super.isSilent();
}
noSilent() {
super.noSilent();
}
closeEmoteMenu(): void { closeEmoteMenu(): void {
if (!this.emoteMenu) return; if (!this.emoteMenu) return;
this.emoteMenu.destroy(); this.emoteMenu.destroy();
@ -119,7 +126,7 @@ export class Player extends Character {
} }
destroy() { destroy() {
this.scene.events.removeListener('postupdate', this.updateListener); this.scene.events.removeListener("postupdate", this.updateListener);
super.destroy(); super.destroy();
} }
} }

View file

@ -1,4 +1,5 @@
import { get, writable } from "svelte/store"; import { get, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
export interface audioManagerVolume { export interface audioManagerVolume {
muted: boolean; muted: boolean;
@ -103,3 +104,7 @@ export const audioManagerVisibilityStore = writable(false);
export const audioManagerVolumeStore = createAudioManagerVolumeStore(); export const audioManagerVolumeStore = createAudioManagerVolumeStore();
export const audioManagerFileStore = createAudioManagerFileStore(); export const audioManagerFileStore = createAudioManagerFileStore();
peerStore.subscribe((peers) => {
audioManagerVolumeStore.setTalking(peers.size > 0);
});

View file

@ -0,0 +1,10 @@
export class MediaStreamConstraintsError extends Error {
static NAME = "MediaStreamConstraintsError";
constructor() {
super(
"Unable to access your camera or microphone. Your browser is too old. Please consider upgrading your browser or try using a recent version of Chrome."
);
this.name = MediaStreamConstraintsError.NAME;
}
}

View file

@ -1,3 +1,4 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export const loginSceneVisibleStore = writable(false); export const loginSceneVisibleStore = writable(false);
export const loginSceneVisibleIframeStore = writable(false);

View file

@ -1,4 +1,4 @@
import { derived, get, Readable, readable, writable, Writable } from "svelte/store"; import { derived, get, Readable, readable, writable } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import { userMovingStore } from "./GameStore"; import { userMovingStore } from "./GameStore";
import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
@ -9,6 +9,7 @@ import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import { peerStore } from "./PeerStore"; import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -170,7 +171,7 @@ function createVideoConstraintStore() {
setFrameRate: (frameRate: number) => setFrameRate: (frameRate: number) =>
update((constraints) => { update((constraints) => {
constraints.frameRate = { ideal: frameRate }; constraints.frameRate = { ideal: frameRate };
localUserStore.setVideoQualityValue(frameRate);
return constraints; return constraints;
}), }),
}; };
@ -255,6 +256,12 @@ export const mediaStreamConstraintsStore = derived(
video: currentVideoConstraint, video: currentVideoConstraint,
audio: currentAudioConstraint, audio: currentAudioConstraint,
}); });
localUserStore.setCameraSetup(
JSON.stringify({
video: currentVideoConstraint,
audio: currentAudioConstraint,
})
);
return; return;
} }
@ -324,14 +331,11 @@ export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
interface StreamSuccessValue { interface StreamSuccessValue {
type: "success"; type: "success";
stream: MediaStream | null; stream: MediaStream | null;
// The constraints that we got (and not the one that have been requested)
constraints: MediaStreamConstraints;
} }
interface StreamErrorValue { interface StreamErrorValue {
type: "error"; type: "error";
error: Error; error: Error;
constraints: MediaStreamConstraints;
} }
let currentStream: MediaStream | null = null; let currentStream: MediaStream | null = null;
@ -339,10 +343,13 @@ let currentStream: MediaStream | null = null;
/** /**
* Stops the camera from filming * Stops the camera from filming
*/ */
function stopCamera(): void { function applyCameraConstraints(currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean): void {
if (currentStream) { if (currentStream) {
for (const track of currentStream.getVideoTracks()) { for (const track of currentStream.getVideoTracks()) {
track.stop(); track.enabled = constraints !== false;
if (constraints && constraints !== true) {
track.applyConstraints(constraints);
}
} }
} }
} }
@ -350,10 +357,16 @@ function stopCamera(): void {
/** /**
* Stops the microphone from listening * Stops the microphone from listening
*/ */
function stopMicrophone(): void { function applyMicrophoneConstraints(
currentStream: MediaStream | null,
constraints: MediaTrackConstraints | boolean
): void {
if (currentStream) { if (currentStream) {
for (const track of currentStream.getAudioTracks()) { for (const track of currentStream.getAudioTracks()) {
track.stop(); track.enabled = constraints !== false;
if (constraints && constraints !== true) {
track.applyConstraints(constraints);
}
} }
} }
} }
@ -372,122 +385,106 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
set({ set({
type: "error", type: "error",
error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."), error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."),
constraints,
}); });
return; return;
} else if (isIOS()) { } else if (isIOS()) {
set({ set({
type: "error", type: "error",
error: new WebviewOnOldIOS(), error: new WebviewOnOldIOS(),
constraints,
}); });
return; return;
} else { } else {
set({ set({
type: "error", type: "error",
error: new BrowserTooOldError(), error: new BrowserTooOldError(),
constraints,
}); });
return; return;
} }
} }
if (constraints.audio === false) { applyMicrophoneConstraints(currentStream, constraints.audio || false);
stopMicrophone(); applyCameraConstraints(currentStream, constraints.video || false);
}
if (constraints.video === false) {
stopCamera();
}
if (constraints.audio === false && constraints.video === false) { if (currentStream === null) {
currentStream = null; // we need to assign a first value to the stream because getUserMedia is async
set({ set({
type: "success", type: "success",
stream: null, stream: null,
constraints,
}); });
return; (async () => {
}
(async () => {
try {
stopMicrophone();
stopCamera();
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: "success",
stream: currentStream,
constraints,
});
return;
} catch (e) {
if (constraints.video !== false) {
console.info(
"Error. Unable to get microphone and/or camera access. Trying audio only.",
$mediaStreamConstraintsStore,
e
);
// TODO: does it make sense to pop this error when retrying?
set({
type: "error",
error: e,
constraints,
});
// Let's try without video constraints
requestedCameraState.disableWebcam();
} else {
console.info(
"Error. Unable to get microphone and/or camera access.",
$mediaStreamConstraintsStore,
e
);
set({
type: "error",
error: e,
constraints,
});
}
/*constraints.video = false;
if (constraints.audio === false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({
type: 'error',
error: e,
constraints
});
// Let's make as if the user did not ask.
requestedCameraState.disableWebcam();
} else {
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
try { try {
currentStream = await navigator.mediaDevices.getUserMedia(constraints); currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({ set({
type: 'success', type: "success",
stream: currentStream, stream: currentStream,
constraints
}); });
return; return;
} catch (e2) { } catch (e) {
console.info("Error. Unable to get microphone fallback access.", $mediaStreamConstraintsStore, e2); if (constraints.video !== false || constraints.audio !== false) {
set({ console.info(
type: 'error', "Error. Unable to get microphone and/or camera access. Trying audio only.",
error: e, $mediaStreamConstraintsStore,
constraints e
}); );
// TODO: does it make sense to pop this error when retrying?
set({
type: "error",
error: e,
});
// Let's try without video constraints
if (constraints.video !== false) {
requestedCameraState.disableWebcam();
}
if (constraints.audio !== false) {
requestedMicrophoneState.disableMicrophone();
}
} else if (!constraints.video && !constraints.audio) {
set({
type: "error",
error: new MediaStreamConstraintsError(),
});
} else {
console.info(
"Error. Unable to get microphone and/or camera access.",
$mediaStreamConstraintsStore,
e
);
set({
type: "error",
error: e,
});
}
} }
}*/ })();
} }
})();
} }
); );
export interface ObtainedMediaStreamConstraints {
video: boolean;
audio: boolean;
}
let obtainedMediaConstraint: ObtainedMediaStreamConstraints = {
audio: false,
video: false,
};
/** /**
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system) * A store containing the actual states of audio and video (activated or deactivated)
*/ */
export const obtainedMediaConstraintStore = derived(localStreamStore, ($localStreamStore) => { export const obtainedMediaConstraintStore = derived<Readable<MediaStreamConstraints>, ObtainedMediaStreamConstraints>(
return $localStreamStore.constraints; mediaStreamConstraintsStore,
}); ($mediaStreamConstraintsStore, set) => {
const newObtainedMediaConstraint = {
video: !!$mediaStreamConstraintsStore.video,
audio: !!$mediaStreamConstraintsStore.audio,
};
if (newObtainedMediaConstraint !== obtainedMediaConstraint) {
obtainedMediaConstraint = newObtainedMediaConstraint;
set(obtainedMediaConstraint);
}
}
);
/** /**
* Device list * Device list
@ -581,3 +578,8 @@ localStreamStore.subscribe((streamResult) => {
* A store containing the real active media is mobile * A store containing the real active media is mobile
*/ */
export const obtainedMediaConstraintIsMobileStore = writable(false); export const obtainedMediaConstraintIsMobileStore = writable(false);
/**
* A store containing if user is silent, so if he is in silent zone. This permit to show et hide camera of user
*/
export const isSilentStore = writable(false);

View file

@ -1,7 +1,13 @@
import { writable } from "svelte/store"; import { get, writable } from "svelte/store";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { userIsAdminStore } from "./GameStore";
import { CONTACT_URL } from "../Enum/EnvironmentVariable";
export const menuIconVisible = writable(false); export const menuIconVisiblilityStore = writable(false);
export const menuVisiblilityStore = writable(false);
export const menuInputFocusStore = writable(false);
export const loginUrlStore = writable(false);
export const userIsConnected = writable(false);
let warningContainerTimeout: Timeout | null = null; let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() { function createWarningContainerStore() {
@ -21,3 +27,90 @@ function createWarningContainerStore() {
} }
export const warningContainerStore = createWarningContainerStore(); export const warningContainerStore = createWarningContainerStore();
export enum SubMenusInterface {
settings = "Settings",
profile = "Profile",
createMap = "Create a Map",
aboutRoom = "About the Room",
globalMessages = "Global Messages",
contact = "Contact",
}
function createSubMenusStore() {
const { subscribe, update } = writable<string[]>([
SubMenusInterface.settings,
SubMenusInterface.profile,
SubMenusInterface.createMap,
SubMenusInterface.aboutRoom,
SubMenusInterface.globalMessages,
SubMenusInterface.contact,
]);
return {
subscribe,
addMenu(menuCommand: string) {
update((menuList: string[]) => {
if (!menuList.find((menu) => menu === menuCommand)) {
menuList.push(menuCommand);
}
return menuList;
});
},
removeMenu(menuCommand: string) {
update((menuList: string[]) => {
const index = menuList.findIndex((menu) => menu === menuCommand);
if (index !== -1) {
menuList.splice(index, 1);
}
return menuList;
});
},
};
}
export const subMenusStore = createSubMenusStore();
function checkSubMenuToShow() {
if (!get(userIsAdminStore)) {
subMenusStore.removeMenu(SubMenusInterface.globalMessages);
}
if (CONTACT_URL === undefined) {
subMenusStore.removeMenu(SubMenusInterface.contact);
}
}
checkSubMenuToShow();
export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>();
export function handleMenuRegistrationEvent(
menuName: string,
iframeUrl: string | undefined = undefined,
source: string | undefined = undefined,
options: { allowApi: boolean }
) {
if (get(subMenusStore).includes(menuName)) {
console.warn("The menu " + menuName + " already exist.");
return;
}
subMenusStore.addMenu(menuName);
if (iframeUrl !== undefined) {
const url = new URL(iframeUrl, source);
customMenuIframe.set(menuName, { url: url.toString(), allowApi: options.allowApi });
}
}
export function handleMenuUnregisterEvent(menuName: string) {
const subMenuGeneral: string[] = Object.values(SubMenusInterface);
if (subMenuGeneral.includes(menuName)) {
console.warn("The menu " + menuName + " is a mandatory menu. It can't be remove");
return;
}
subMenusStore.removeMenu(menuName);
customMenuIframe.delete(menuName);
}

View file

@ -1,38 +1,30 @@
import { readable, writable } from "svelte/store"; import { readable, writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer"; import type { VideoPeer } from "../WebRtc/VideoPeer";
import { VideoPeer } from "../WebRtc/VideoPeer"; import type { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
/** /**
* A store that contains the list of (video) peers we are connected to. * A store that contains the list of (video) peers we are connected to.
*/ */
function createPeerStore() { function createPeerStore() {
let peers = new Map<number, VideoPeer>(); const { subscribe, set, update } = writable(new Map<number, VideoPeer>());
const { subscribe, set, update } = writable(peers);
return { return {
subscribe, subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => { pushNewPeer(peer: VideoPeer) {
peers = new Map<number, VideoPeer>(); update((users) => {
set(peers); users.set(peer.userId, peer);
simplePeer.registerPeerConnectionListener({ return users;
onConnect(peer: RemotePeer) {
if (peer instanceof VideoPeer) {
update((users) => {
users.set(peer.userId, peer);
return users;
});
}
},
onDisconnect(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
}); });
}, },
removePeer(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
cleanupStore() {
set(new Map<number, VideoPeer>());
},
}; };
} }
@ -40,32 +32,25 @@ function createPeerStore() {
* A store that contains the list of screen sharing peers we are connected to. * A store that contains the list of screen sharing peers we are connected to.
*/ */
function createScreenSharingPeerStore() { function createScreenSharingPeerStore() {
let peers = new Map<number, ScreenSharingPeer>(); const { subscribe, set, update } = writable(new Map<number, ScreenSharingPeer>());
const { subscribe, set, update } = writable(peers);
return { return {
subscribe, subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => { pushNewPeer(peer: ScreenSharingPeer) {
peers = new Map<number, ScreenSharingPeer>(); update((users) => {
set(peers); users.set(peer.userId, peer);
simplePeer.registerPeerConnectionListener({ return users;
onConnect(peer: RemotePeer) {
if (peer instanceof ScreenSharingPeer) {
update((users) => {
users.set(peer.userId, peer);
return users;
});
}
},
onDisconnect(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
}); });
}, },
removePeer(userId: number) {
update((users) => {
users.delete(userId);
return users;
});
},
cleanupStore() {
set(new Map<number, ScreenSharingPeer>());
},
}; };
} }

View file

@ -2,6 +2,7 @@ import { writable } from "svelte/store";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection"; import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator"; import { getRandomColor } from "../WebRtc/ColorGenerator";
import { localUserStore } from "../Connexion/LocalUserStore";
let idCount = 0; let idCount = 0;

View file

@ -1,7 +1,6 @@
import { derived, get, Readable, readable, writable, Writable } from "svelte/store"; import { derived, Readable, readable, writable } from "svelte/store";
import { peerStore } from "./PeerStore"; import { peerStore } from "./PeerStore";
import type { LocalStreamStoreValue } from "./MediaStore"; import type { LocalStreamStoreValue } from "./MediaStore";
import { DivImportance } from "../WebRtc/LayoutManager";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -106,7 +105,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({ set({
type: "success", type: "success",
stream: null, stream: null,
constraints,
}); });
return; return;
} }
@ -121,7 +119,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({ set({
type: "error", type: "error",
error: new Error("Your browser does not support sharing screen"), error: new Error("Your browser does not support sharing screen"),
constraints,
}); });
return; return;
} }
@ -141,10 +138,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({ set({
type: "success", type: "success",
stream: null, stream: null,
constraints: {
video: false,
audio: false,
},
}); });
}; };
} }
@ -152,7 +145,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({ set({
type: "success", type: "success",
stream: currentStream, stream: currentStream,
constraints,
}); });
return; return;
} catch (e) { } catch (e) {
@ -162,7 +154,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({ set({
type: "error", type: "error",
error: e, error: e,
constraints,
}); });
} }
})(); })();
@ -184,7 +175,7 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
export interface ScreenSharingLocalMedia { export interface ScreenSharingLocalMedia {
uniqueId: string; uniqueId: string;
stream: MediaStream | null; stream: MediaStream | null;
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; userId?: undefined;
} }
/** /**

View file

@ -1,3 +1,8 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export const showReportScreenStore = writable<{ userId: number; userName: string } | null>(null); export const userReportEmpty = {
userId: 0,
userName: "Empty",
};
export const showReportScreenStore = writable<{ userId: number; userName: string }>(userReportEmpty);

View file

@ -1,11 +1,12 @@
import { derived } from "svelte/store"; import { derived } from "svelte/store";
import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore"; import { menuInputFocusStore } from "./MenuStore";
import { chatInputFocusStore } from "./ChatStore"; import { chatInputFocusStore } from "./ChatStore";
import { showReportScreenStore, userReportEmpty } from "./ShowReportScreenStore";
//derived from the focus on Menu, ConsoleGlobal, Chat and ... //derived from the focus on Menu, ConsoleGlobal, Chat and ...
export const enableUserInputsStore = derived( export const enableUserInputsStore = derived(
[consoleGlobalMessageManagerFocusStore, chatInputFocusStore], [menuInputFocusStore, chatInputFocusStore, showReportScreenStore],
([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => { ([$menuInputFocusStore, $chatInputFocusStore, $showReportScreenStore]) => {
return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore; return !$menuInputFocusStore && !$chatInputFocusStore && !($showReportScreenStore !== userReportEmpty);
} }
); );

View file

@ -1,14 +1,12 @@
import { writable } from "svelte/store"; import { get, writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import { VideoPeer } from "../WebRtc/VideoPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import type { Streamable } from "./StreamableCollectionStore"; import type { Streamable } from "./StreamableCollectionStore";
import { peerStore } from "./PeerStore";
/** /**
* A store that contains the peer / media that has currently the "importance" focus. * A store that contains the peer / media that has currently the "importance" focus.
*/ */
function createVideoFocusStore() { function createVideoFocusStore() {
const { subscribe, set, update } = writable<Streamable | null>(null); const { subscribe, set } = writable<Streamable | null>(null);
let focusedMedia: Streamable | null = null; let focusedMedia: Streamable | null = null;
@ -23,27 +21,17 @@ function createVideoFocusStore() {
set(null); set(null);
}, },
toggleFocus: (media: Streamable) => { toggleFocus: (media: Streamable) => {
if (media !== focusedMedia) { focusedMedia = media !== focusedMedia ? media : null;
focusedMedia = media;
} else {
focusedMedia = null;
}
set(focusedMedia); set(focusedMedia);
}, },
connectToSimplePeer: (simplePeer: SimplePeer) => {
simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) {},
onDisconnect(userId: number) {
if (
(focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) &&
focusedMedia.userId === userId
) {
set(null);
}
},
});
},
}; };
} }
export const videoFocusStore = createVideoFocusStore(); export const videoFocusStore = createVideoFocusStore();
peerStore.subscribe((peers) => {
const focusedMedia: Streamable | null = get(videoFocusStore);
if (focusedMedia && focusedMedia.userId !== undefined && !peers.get(focusedMedia.userId)) {
videoFocusStore.removeFocus();
}
});

View file

@ -1,4 +1,5 @@
import type { Room } from "../Connexion/Room"; import type { Room } from "../Connexion/Room";
import { localUserStore } from "../Connexion/LocalUserStore";
export enum GameConnexionTypes { export enum GameConnexionTypes {
anonymous = 1, anonymous = 1,
@ -7,13 +8,16 @@ export enum GameConnexionTypes {
empty, empty,
unknown, unknown,
jwt, jwt,
login,
} }
//this class is responsible with analysing and editing the game's url //this class is responsible with analysing and editing the game's url
class UrlManager { class UrlManager {
public getGameConnexionType(): GameConnexionTypes { public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString(); const url = window.location.pathname.toString();
if (url === "/jwt") { if (url === "/login") {
return GameConnexionTypes.login;
} else if (url === "/jwt") {
return GameConnexionTypes.jwt; return GameConnexionTypes.jwt;
} else if (url.includes("_/")) { } else if (url.includes("_/")) {
return GameConnexionTypes.anonymous; return GameConnexionTypes.anonymous;
@ -35,6 +39,8 @@ class UrlManager {
public pushRoomIdToUrl(room: Room): void { public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id) return; if (window.location.pathname === room.id) return;
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
localUserStore.setLastRoomUrl(room.key);
const hash = window.location.hash; const hash = window.location.hash;
const search = room.search.toString(); const search = room.search.toString();
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash); history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);

View file

@ -48,6 +48,10 @@ class CoWebsiteManager {
this.cowebsiteDiv.style.width = width + "px"; this.cowebsiteDiv.style.width = width + "px";
} }
set widthPercent(width: number) {
this.cowebsiteDiv.style.width = width + "%";
}
get height(): number { get height(): number {
return this.cowebsiteDiv.clientHeight; return this.cowebsiteDiv.clientHeight;
} }
@ -162,7 +166,13 @@ class CoWebsiteManager {
return iframe; return iframe;
} }
public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void { public loadCoWebsite(
url: string,
base: string,
allowApi?: boolean,
allowPolicy?: string,
widthPercent?: number
): void {
this.load(); this.load();
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
@ -186,6 +196,9 @@ class CoWebsiteManager {
.then(() => Promise.race([onloadPromise, onTimeoutPromise])) .then(() => Promise.race([onloadPromise, onTimeoutPromise]))
.then(() => { .then(() => {
this.open(); this.open();
if (widthPercent) {
this.widthPercent = widthPercent;
}
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime); }, animationTime);
@ -199,13 +212,16 @@ class CoWebsiteManager {
/** /**
* Just like loadCoWebsite but the div can be filled by the user. * Just like loadCoWebsite but the div can be filled by the user.
*/ */
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void { public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>, widthPercent?: number): void {
this.load(); this.load();
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
this.currentOperationPromise = this.currentOperationPromise this.currentOperationPromise = this.currentOperationPromise
.then(() => callback(this.cowebsiteMainDom)) .then(() => callback(this.cowebsiteMainDom))
.then(() => { .then(() => {
this.open(); this.open();
if (widthPercent) {
this.widthPercent = widthPercent;
}
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime); }, animationTime);

View file

@ -1,37 +1,67 @@
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import { JITSI_URL } from "../Enum/EnvironmentVariable";
import {mediaManager} from "./MediaManager"; import { coWebsiteManager } from "./CoWebsiteManager";
import {coWebsiteManager} from "./CoWebsiteManager"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore"; import { get } from "svelte/store";
import {get} from "svelte/store";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
interface jitsiConfigInterface { interface jitsiConfigInterface {
startWithAudioMuted: boolean startWithAudioMuted: boolean;
startWithVideoMuted: boolean startWithVideoMuted: boolean;
prejoinPageEnabled: boolean prejoinPageEnabled: boolean;
} }
const getDefaultConfig = () : jitsiConfigInterface => { interface JitsiOptions {
return { jwt?: string;
startWithAudioMuted: !get(requestedMicrophoneState), roomName: string;
startWithVideoMuted: !get(requestedCameraState), width: string;
prejoinPageEnabled: false height: string;
parentNode: HTMLElement;
configOverwrite: jitsiConfigInterface;
interfaceConfigOverwrite: typeof defaultInterfaceConfig;
onload?: Function;
}
interface JitsiApi {
executeCommand: (command: string, ...args: Array<unknown>) => void;
addListener: (type: string, callback: Function) => void;
removeListener: (type: string, callback: Function) => void;
dispose: () => void;
}
declare global {
interface Window {
JitsiMeetExternalAPI: new (domain: string, options: JitsiOptions) => JitsiApi;
} }
} }
const getDefaultConfig = (): jitsiConfigInterface => {
return {
startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false,
};
};
const mergeConfig = (config?: object) => { const mergeConfig = (config?: object) => {
const currentDefaultConfig = getDefaultConfig(); const currentDefaultConfig = getDefaultConfig();
if(!config){ if (!config) {
return currentDefaultConfig; return currentDefaultConfig;
} }
return { return {
...currentDefaultConfig, ...currentDefaultConfig,
...config, ...config,
startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted ? true : currentDefaultConfig.startWithAudioMuted, startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted
startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted ? true : currentDefaultConfig.startWithVideoMuted, ? true
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled ? true : currentDefaultConfig.prejoinPageEnabled : currentDefaultConfig.startWithAudioMuted,
} startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted
} ? true
: currentDefaultConfig.startWithVideoMuted,
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled
? true
: currentDefaultConfig.prejoinPageEnabled,
};
};
const defaultInterfaceConfig = { const defaultInterfaceConfig = {
SHOW_CHROME_EXTENSION_BANNER: false, SHOW_CHROME_EXTENSION_BANNER: false,
@ -49,28 +79,48 @@ const defaultInterfaceConfig = {
SHOW_WATERMARK_FOR_GUESTS: false, SHOW_WATERMARK_FOR_GUESTS: false,
TOOLBAR_BUTTONS: [ TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', /*'embedmeeting',*/ 'fullscreen', "microphone",
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', "camera",
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', "closedcaptions",
'videoquality', 'filmstrip', /*'invite',*/ 'feedback', 'stats', 'shortcuts', "desktop",
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', /*'security'*/ /*'embedmeeting',*/ "fullscreen",
"fodeviceselection",
"hangup",
"profile",
"chat",
"recording",
"livestreaming",
"etherpad",
"sharedvideo",
"settings",
"raisehand",
"videoquality",
"filmstrip",
/*'invite',*/ "feedback",
"stats",
"shortcuts",
"tileview",
"videobackgroundblur",
"download",
"help",
"mute-everyone" /*'security'*/,
], ],
}; };
const slugify = (...args: (string | number)[]): string => { const slugify = (...args: (string | number)[]): string => {
const value = args.join(' ') const value = args.join(" ");
return value return value
.normalize('NFD') // split an accented letter in the base letter and the accent .normalize("NFD") // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced) .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator .replace(/\s+/g, "-"); // separator
} };
class JitsiFactory { class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any private jitsiApi?: JitsiApi;
private audioCallback = this.onAudioChange.bind(this); private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this); private videoCallback = this.onVideoChange.bind(this);
private jitsiScriptLoaded: boolean = false; private jitsiScriptLoaded: boolean = false;
@ -79,11 +129,19 @@ class JitsiFactory {
* Slugifies the room name and prepends the room name with the instance * Slugifies the room name and prepends the room name with the instance
*/ */
public getRoomName(roomName: string, instance: string): string { public getRoomName(roomName: string, instance: string): string {
return slugify(instance.replace('/', '-') + "-" + roomName); return slugify(instance.replace("/", "-") + "-" + roomName);
} }
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void { public start(
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => { roomName: string,
playerName: string,
jwt?: string,
config?: object,
interfaceConfig?: object,
jitsiUrl?: string,
jitsiWidth?: number
): void {
coWebsiteManager.insertCoWebsite(async (cowebsiteDiv) => {
// Jitsi meet external API maintains some data in local storage // Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a // which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus // conference. Problem is that this data grows indefinitely. Thus
@ -94,18 +152,18 @@ class JitsiFactory {
const domain = jitsiUrl || JITSI_URL; const domain = jitsiUrl || JITSI_URL;
if (domain === undefined) { if (domain === undefined) {
throw new Error('Missing JITSI_URL environment variable or jitsiUrl parameter in the map.') throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
} }
await this.loadJitsiScript(domain); await this.loadJitsiScript(domain);
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any const options: JitsiOptions = {
roomName: roomName, roomName: roomName,
jwt: jwt, jwt: jwt,
width: "100%", width: "100%",
height: "100%", height: "100%",
parentNode: cowebsiteDiv, parentNode: cowebsiteDiv,
configOverwrite: mergeConfig(config), configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig} interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
}; };
if (!options.jwt) { if (!options.jwt) {
delete options.jwt; delete options.jwt;
@ -115,25 +173,25 @@ class JitsiFactory {
options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations. options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand('displayName', playerName); this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback); this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback); this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
}); });
})); }, jitsiWidth);
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
if(!this.jitsiApi){ if (!this.jitsiApi) {
return; return;
} }
await coWebsiteManager.closeCoWebsite(); await coWebsiteManager.closeCoWebsite();
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback); this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback); this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback);
this.jitsiApi?.dispose(); this.jitsiApi?.dispose();
} }
private onAudioChange({muted}: {muted: boolean}): void { private onAudioChange({ muted }: { muted: boolean }): void {
if (muted) { if (muted) {
requestedMicrophoneState.disableMicrophone(); requestedMicrophoneState.disableMicrophone();
} else { } else {
@ -141,7 +199,7 @@ class JitsiFactory {
} }
} }
private onVideoChange({muted}: {muted: boolean}): void { private onVideoChange({ muted }: { muted: boolean }): void {
if (muted) { if (muted) {
requestedCameraState.disableWebcam(); requestedCameraState.disableWebcam();
} else { } else {
@ -159,20 +217,17 @@ class JitsiFactory {
this.jitsiScriptLoaded = true; this.jitsiScriptLoaded = true;
// Load Jitsi if the environment variable is set. // Load Jitsi if the environment variable is set.
const jitsiScript = document.createElement('script'); const jitsiScript = document.createElement("script");
jitsiScript.src = 'https://' + domain + '/external_api.js'; jitsiScript.src = "https://" + domain + "/external_api.js";
jitsiScript.onload = () => { jitsiScript.onload = () => {
resolve(); resolve();
} };
jitsiScript.onerror = () => { jitsiScript.onerror = () => {
reject(); reject();
} };
document.head.appendChild(jitsiScript); document.head.appendChild(jitsiScript);
});
})
} }
} }

View file

@ -11,6 +11,8 @@ import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore"; import { layoutManagerActionStore, layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore";
import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError";
export class MediaManager { export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
@ -23,16 +25,17 @@ export class MediaManager {
constructor() { constructor() {
localStreamStore.subscribe((result) => { localStreamStore.subscribe((result) => {
if (result.type === "error") { if (result.type === "error") {
console.error(result.error); if (result.error.name !== MediaStreamConstraintsError.NAME) {
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: "cameraAccessDenied", uuid: "cameraAccessDenied",
type: "warning", type: "warning",
message: "Camera access denied. Click here and check your browser permissions.", message: "Camera access denied. Click here and check your browser permissions.",
callback: () => { callback: () => {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
}, },
userInputManager: this.userInputManager, userInputManager: this.userInputManager,
}); });
}
//remove it after 10 sec //remove it after 10 sec
setTimeout(() => { setTimeout(() => {
layoutManagerActionStore.removeAction("cameraAccessDenied"); layoutManagerActionStore.removeAction("cameraAccessDenied");
@ -187,7 +190,11 @@ export class MediaManager {
} }
public hasNotification(): boolean { public hasNotification(): boolean {
return Notification.permission === "granted"; if (Notification.permission === "granted") {
return localUserStore.getNotification() === "granted";
} else {
return false;
}
} }
public requestNotification() { public requestNotification() {

View file

@ -4,16 +4,13 @@ import type {
} from "../Connexion/ConnexionModels"; } from "../Connexion/ConnexionModels";
import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager"; import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
import { ScreenSharingPeer } from "./ScreenSharingPeer"; import { ScreenSharingPeer } from "./ScreenSharingPeer";
import { MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer } from "./VideoPeer"; import { VideoPeer } from "./VideoPeer";
import type { RoomConnection } from "../Connexion/RoomConnection"; import type { RoomConnection } from "../Connexion/RoomConnection";
import { blackListManager } from "./BlackListManager"; import { blackListManager } from "./BlackListManager";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { discussionManager } from "./DiscussionManager";
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { newChatMessageStore } from "../Stores/ChatStore"; import { peerStore, screenSharingPeerStore } from "../Stores/PeerStore";
import { isMobile } from "../Enum/EnvironmentVariable";
export interface UserSimplePeerInterface { export interface UserSimplePeerInterface {
userId: number; userId: number;
@ -24,12 +21,6 @@ export interface UserSimplePeerInterface {
export type RemotePeer = VideoPeer | ScreenSharingPeer; export type RemotePeer = VideoPeer | ScreenSharingPeer;
export interface PeerConnectionListener {
onConnect(user: RemotePeer): void;
onDisconnect(userId: number): void;
}
/** /**
* This class manages connections to all the peers in the same group as me. * This class manages connections to all the peers in the same group as me.
*/ */
@ -41,24 +32,21 @@ export class SimplePeer {
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback; private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback; private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly unsubscribers: (() => void)[] = []; private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number; private readonly userId: number;
private lastWebrtcUserName: string | undefined; private lastWebrtcUserName: string | undefined;
private lastWebrtcPassword: string | undefined; private lastWebrtcPassword: string | undefined;
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) { constructor(private Connection: RoomConnection) {
//we make sure we don't get any old peer.
peerStore.cleanupStore();
screenSharingPeerStore.cleanupStore();
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this); this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
this.unsubscribers.push(
localStreamStore.subscribe((streamResult) => {
this.sendLocalVideoStream(streamResult);
})
);
let localScreenCapture: MediaStream | null = null; let localScreenCapture: MediaStream | null = null;
//todo
this.unsubscribers.push( this.unsubscribers.push(
screenSharingLocalStreamStore.subscribe((streamResult) => { screenSharingLocalStreamStore.subscribe((streamResult) => {
if (streamResult.type === "error") { if (streamResult.type === "error") {
@ -82,14 +70,6 @@ export class SimplePeer {
this.initialise(); this.initialise();
} }
public registerPeerConnectionListener(peerConnectionListener: PeerConnectionListener) {
this.peerConnectionListeners.push(peerConnectionListener);
}
public getNbConnections(): number {
return this.Users.length;
}
/** /**
* permit to listen when user could start visio * permit to listen when user could start visio
*/ */
@ -126,19 +106,14 @@ export class SimplePeer {
if (!user.initiator) { if (!user.initiator) {
return; return;
} }
const streamResult = get(localStreamStore);
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream;
}
this.createPeerConnection(user, stream); this.createPeerConnection(user);
} }
/** /**
* create peer connection to bind users * create peer connection to bind users
*/ */
private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null { private createPeerConnection(user: UserSimplePeerInterface): VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId); const peerConnection = this.PeerConnectionArray.get(user.userId);
if (peerConnection) { if (peerConnection) {
if (peerConnection.destroyed) { if (peerConnection.destroyed) {
@ -160,7 +135,7 @@ export class SimplePeer {
this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcUserName = user.webRtcUser;
this.lastWebrtcPassword = user.webRtcPassword; this.lastWebrtcPassword = user.webRtcPassword;
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection);
peer.toClose = false; peer.toClose = false;
// When a connection is established to a video stream, and if a screen sharing is taking place, // When a connection is established to a video stream, and if a screen sharing is taking place,
@ -178,9 +153,7 @@ export class SimplePeer {
} }
this.PeerConnectionArray.set(user.userId, peer); this.PeerConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { peerStore.pushNewPeer(peer);
peerConnectionListener.onConnect(peer);
}
return peer; return peer;
} }
@ -204,7 +177,7 @@ export class SimplePeer {
if (!peerConnexionDeleted) { if (!peerConnexionDeleted) {
throw "Error to delete peer connection"; throw "Error to delete peer connection";
} }
this.createPeerConnection(user, stream); this.createPeerConnection(user);
} else { } else {
peerConnection.toClose = false; peerConnection.toClose = false;
} }
@ -228,9 +201,7 @@ export class SimplePeer {
); );
this.PeerScreenSharingConnectionArray.set(user.userId, peer); this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { screenSharingPeerStore.pushNewPeer(peer);
peerConnectionListener.onConnect(peer);
}
return peer; return peer;
} }
@ -269,12 +240,11 @@ export class SimplePeer {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) { for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId); this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId); this.PeerScreenSharingConnectionArray.delete(userId);
screenSharingPeerStore.removePeer(userId);
} }
} }
for (const peerConnectionListener of this.peerConnectionListeners) { peerStore.removePeer(userId);
peerConnectionListener.onDisconnect(userId);
}
} }
/** /**
@ -282,7 +252,6 @@ export class SimplePeer {
*/ */
private closeScreenSharingConnection(userId: number) { private closeScreenSharingConnection(userId: number) {
try { try {
//mediaManager.removeActiveScreenSharingVideo("" + userId);
const peer = this.PeerScreenSharingConnectionArray.get(userId); const peer = this.PeerScreenSharingConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
console.warn( console.warn(
@ -295,12 +264,6 @@ export class SimplePeer {
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
peer.destroy(); peer.destroy();
//Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
/*if(!this.PeerScreenSharingConnectionArray.delete(userId)){
throw 'Couln\'t delete peer screen sharing connexion';
}*/
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) { } catch (err) {
console.error("closeConnection", err); console.error("closeConnection", err);
} }
@ -323,6 +286,8 @@ export class SimplePeer {
for (const unsubscriber of this.unsubscribers) { for (const unsubscriber of this.unsubscribers) {
unsubscriber(); unsubscriber();
} }
peerStore.cleanupStore();
screenSharingPeerStore.cleanupStore();
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -330,13 +295,7 @@ export class SimplePeer {
try { try {
//if offer type, create peer connection //if offer type, create peer connection
if (data.signal.type === "offer") { if (data.signal.type === "offer") {
const streamResult = get(localStreamStore); this.createPeerConnection(data);
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream;
}
this.createPeerConnection(data, stream);
} }
const peer = this.PeerConnectionArray.get(data.userId); const peer = this.PeerConnectionArray.get(data.userId);
if (peer !== undefined) { if (peer !== undefined) {
@ -352,7 +311,6 @@ export class SimplePeer {
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) { private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
const uuid = playersStore.getPlayerById(data.userId)?.userUuid || ""; const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return; if (blackListManager.isBlackListed(uuid)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore); const streamResult = get(screenSharingLocalStreamStore);
let stream: MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream !== null) { if (streamResult.type === "success" && streamResult.stream !== null) {
@ -379,48 +337,10 @@ export class SimplePeer {
} catch (e) { } catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e); console.error(`receiveWebrtcSignal => ${data.userId}`, e);
//Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion //Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
//this.PeerScreenSharingConnectionArray.delete(data.userId);
this.receiveWebrtcScreenSharingSignal(data); this.receiveWebrtcScreenSharingSignal(data);
} }
} }
private pushVideoToRemoteUser(userId: number, streamResult: LocalStreamStoreValue) {
try {
const PeerConnection = this.PeerConnectionArray.get(userId);
if (!PeerConnection) {
throw new Error("While adding media, cannot find user with ID " + userId);
}
PeerConnection.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT,
...streamResult.constraints,
isMobile: isMobile(),
})
)
);
if (streamResult.type === "error") {
return;
}
const localStream: MediaStream | null = streamResult.stream;
if (!localStream) {
return;
}
for (const track of localStream.getTracks()) {
//todo: this is a ugly hack to reduce the amount of error in console. Find a better way.
if ((track as any).added !== undefined) continue; // eslint-disable-line @typescript-eslint/no-explicit-any
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
PeerConnection.addTrack(track, localStream);
}
} catch (e) {
console.error(`pushVideoToRemoteUser => ${userId}`, e);
}
}
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) { private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId); const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) { if (!PeerConnection) {
@ -433,12 +353,6 @@ export class SimplePeer {
return; return;
} }
public sendLocalVideoStream(streamResult: LocalStreamStoreValue) {
for (const user of this.Users) {
this.pushVideoToRemoteUser(user.userId, streamResult);
}
}
/** /**
* Triggered locally when clicking on the screen sharing button * Triggered locally when clicking on the screen sharing button
*/ */
@ -492,8 +406,6 @@ export class SimplePeer {
if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) { if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) {
PeerConnectionScreenSharing.destroy(); PeerConnectionScreenSharing.destroy();
//Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
//this.PeerScreenSharingConnectionArray.delete(userId);
} }
} }
} }

View file

@ -4,10 +4,15 @@ import type { RoomConnection } from "../Connexion/RoomConnection";
import { blackListManager } from "./BlackListManager"; import { blackListManager } from "./BlackListManager";
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import type { UserSimplePeerInterface } from "./SimplePeer"; import type { UserSimplePeerInterface } from "./SimplePeer";
import { get, readable, Readable, Unsubscriber } from "svelte/store"; import { readable, Readable, Unsubscriber } from "svelte/store";
import { obtainedMediaConstraintIsMobileStore, obtainedMediaConstraintStore } from "../Stores/MediaStore"; import {
localStreamStore,
obtainedMediaConstraintIsMobileStore,
obtainedMediaConstraintStore,
ObtainedMediaStreamConstraints,
} from "../Stores/MediaStore";
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore"; import { chatMessagesStore, newChatMessageStore } from "../Stores/ChatStore";
import { getIceServersConfig } from "../Components/Video/utils"; import { getIceServersConfig } from "../Components/Video/utils";
import { isMobile } from "../Enum/EnvironmentVariable"; import { isMobile } from "../Enum/EnvironmentVariable";
@ -34,16 +39,17 @@ export class VideoPeer extends Peer {
private onUnBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>; public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>; public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints | null>; public readonly constraintsStore: Readable<ObtainedMediaStreamConstraints | null>;
private newMessageunsubscriber: Unsubscriber | null = null; private newMessageunsubscriber: Unsubscriber | null = null;
private closing: Boolean = false; //this is used to prevent destroy() from being called twice private closing: Boolean = false; //this is used to prevent destroy() from being called twice
private localStreamStoreSubscribe: Unsubscriber;
private obtainedMediaConstraintStoreSubscribe: Unsubscriber;
constructor( constructor(
public user: UserSimplePeerInterface, public user: UserSimplePeerInterface,
initiator: boolean, initiator: boolean,
public readonly userName: string, public readonly userName: string,
private connection: RoomConnection, private connection: RoomConnection
localStream: MediaStream | null
) { ) {
super({ super({
initiator, initiator,
@ -60,27 +66,15 @@ export class VideoPeer extends Peer {
const onStream = (stream: MediaStream | null) => { const onStream = (stream: MediaStream | null) => {
set(stream); set(stream);
}; };
const onData = (chunk: Buffer) => {
this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (!message.video) {
set(null);
}
}
});
};
this.on("stream", onStream); this.on("stream", onStream);
this.on("data", onData);
return () => { return () => {
this.off("stream", onStream); this.off("stream", onStream);
this.off("data", onData);
}; };
}); });
this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => { this.constraintsStore = readable<ObtainedMediaStreamConstraints | null>(null, (set) => {
const onData = (chunk: Buffer) => { const onData = (chunk: Buffer) => {
const message = JSON.parse(chunk.toString("utf8")); const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
@ -191,7 +185,6 @@ export class VideoPeer extends Peer {
this._onFinish(); this._onFinish();
}); });
this.pushVideoToRemoteUser(localStream);
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => { this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => {
if (userUuid === this.userUuid) { if (userUuid === this.userUuid) {
this.toggleRemoteStream(false); this.toggleRemoteStream(false);
@ -208,6 +201,21 @@ export class VideoPeer extends Peer {
if (blackListManager.isBlackListed(this.userUuid)) { if (blackListManager.isBlackListed(this.userUuid)) {
this.sendBlockMessage(true); this.sendBlockMessage(true);
} }
this.localStreamStoreSubscribe = localStreamStore.subscribe((streamValue) => {
if (streamValue.type === "success" && streamValue.stream) this.addStream(streamValue.stream);
});
this.obtainedMediaConstraintStoreSubscribe = obtainedMediaConstraintStore.subscribe((constraints) => {
this.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT,
...constraints,
isMobile: isMobile(),
})
)
);
});
} }
private sendBlockMessage(blocking: boolean) { private sendBlockMessage(blocking: boolean) {
@ -264,6 +272,8 @@ export class VideoPeer extends Peer {
this.onUnBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe();
if (this.newMessageunsubscriber) this.newMessageunsubscriber(); if (this.newMessageunsubscriber) this.newMessageunsubscriber();
chatMessagesStore.addOutcomingUser(this.userId); chatMessagesStore.addOutcomingUser(this.userId);
if (this.localStreamStoreSubscribe) this.localStreamStoreSubscribe();
if (this.obtainedMediaConstraintStoreSubscribe) this.obtainedMediaConstraintStoreSubscribe();
super.destroy(); super.destroy();
} catch (err) { } catch (err) {
console.error("VideoPeer::destroy", err); console.error("VideoPeer::destroy", err);
@ -281,28 +291,4 @@ export class VideoPeer extends Peer {
this.once("connect", destroySoon); this.once("connect", destroySoon);
} }
} }
private pushVideoToRemoteUser(localStream: MediaStream | null) {
try {
this.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT,
...get(obtainedMediaConstraintStore),
isMobile: isMobile(),
})
)
);
if (!localStream) {
return;
}
for (const track of localStream.getTracks()) {
this.addTrack(track, localStream);
}
} catch (e) {
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
}
}
} }

View file

@ -13,7 +13,6 @@ import WebFontLoaderPlugin from "phaser3-rex-plugins/plugins/webfontloader-plugi
import OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { EntryScene } from "./Phaser/Login/EntryScene"; import { EntryScene } from "./Phaser/Login/EntryScene";
import { coWebsiteManager } from "./WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "./WebRtc/CoWebsiteManager";
import { MenuScene } from "./Phaser/Menu/MenuScene";
import { localUserStore } from "./Connexion/LocalUserStore"; import { localUserStore } from "./Connexion/LocalUserStore";
import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene"; import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene";
import { iframeListener } from "./Api/IframeListener"; import { iframeListener } from "./Api/IframeListener";
@ -97,7 +96,6 @@ const config: GameConfig = {
ReconnectingScene, ReconnectingScene,
ErrorScene, ErrorScene,
CustomizeScene, CustomizeScene,
MenuScene,
], ],
//resolution: window.devicePixelRatio / 2, //resolution: window.devicePixelRatio / 2,
fps: fps, fps: fps,

View file

@ -1,10 +1,10 @@
import type Phaser from "phaser"; import type Phaser from "phaser";
export type CursorKey = { export type CursorKey = {
isDown: boolean isDown: boolean;
} };
export type Direction = 'left' | 'right' | 'up' | 'down' export type Direction = "left" | "right" | "up" | "down";
export interface CursorKeys extends Record<Direction, CursorKey> { export interface CursorKeys extends Record<Direction, CursorKey> {
left: CursorKey; left: CursorKey;
@ -21,4 +21,3 @@ export interface IVirtualJoystick extends Phaser.GameObjects.GameObject {
visible: boolean; visible: boolean;
createCursorKeys: () => CursorKeys; createCursorKeys: () => CursorKeys;
} }

View file

@ -1,15 +1,15 @@
//InputTextGlobalMessage //TextGlobalMessage
section.section-input-send-text { section.section-input-send-text {
--height-toolbar: 15%;
height: 100%; height: 100%;
.ql-toolbar{ .ql-toolbar{
max-height: 100px; height: var(--height-toolbar);
background: whitesmoke; background: whitesmoke;
} }
div.input-send-text{ div.input-send-text{
height: calc(100% - 100px); height: calc(100% - var(--height-toolbar));
overflow: auto; overflow: auto;
color: whitesmoke; color: whitesmoke;
@ -29,3 +29,13 @@ section.section-input-send-text {
} }
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
section.section-input-send-text {
--height-toolbar: 30%;
.ql-toolbar {
overflow: auto;
}
}
}

View file

@ -3,4 +3,4 @@
@import "style"; @import "style";
@import "mobile-style.scss"; @import "mobile-style.scss";
@import "fonts.scss"; @import "fonts.scss";
@import "inputTextGlobalMessageSvelte-Style.scss"; @import "TextGlobalMessageSvelte-Style";

View file

@ -1,5 +1,4 @@
*{ *{
font-family: Lato;
cursor: url('./images/cursor_normal.png'), auto; cursor: url('./images/cursor_normal.png'), auto;
} }
* a, button, select{ * a, button, select{
@ -400,62 +399,6 @@ body {
} }
} }
.audioplayer:first-child {
display: grid;
grid: 2rem / 4rem 10rem;
}
.audioplayer > button, .audioplayer > div, .audioplayer > label {
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.audioplayer > div {
padding-right: 1.2rem;
}
#audioplayerctrl {
position: fixed;
top: 0;
right: calc(50% - 120px);
padding: 0.3rem 0.5rem;
color: white;
transition: transform 0.5s;
}
#audioplayer_mute {
max-width: 5rem;
border: none;
}
#audioplayer_mute:focus, #audioplayer_mute:active {
outline: none;
}
#audioplayer_mute > svg {
width: 100%;
height: 100%;
pointer-events: none;
}
#audioplayer_volume_icon_playing.muted {
visibility: hidden;
display: none;
}
#audioplayerctrl > #audioplayer_volume {
width: 100%;
background-color: rgba(0,0,0,0.5);
}
/*
* sollte eigentlich in den aspect-ratio teil ..
*/
#audioplayerctrl.loading {
transform: translateY(-90%);
}
#audioplayerctrl.hidden {
transform: translateY(-100%);
}
/* /*
* Style Input Range * Style Input Range
* https://www.cssportal.com/style-input-range/ * https://www.cssportal.com/style-input-range/
@ -1140,3 +1083,19 @@ div.action.danger p.action-body{
} }
} }
} }
div.is-silent {
position: absolute;
bottom: 40px;
border-radius: 15px 15px 15px 15px;
max-height: 20%;
transition: right 350ms;
right: -20vw;
background-color: black;
font-size: 20px;
color: white;
padding: 30px 20px;
}
div.is-silent.hide {
right: 15px;
}

View file

@ -7,6 +7,7 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
import sveltePreprocess from "svelte-preprocess"; import sveltePreprocess from "svelte-preprocess";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import { PROFILE_URL } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development"; const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -190,6 +191,8 @@ module.exports = {
PUSHER_URL: undefined, PUSHER_URL: undefined,
UPLOADER_URL: null, UPLOADER_URL: null,
ADMIN_URL: undefined, ADMIN_URL: undefined,
CONTACT_URL: null,
PROFILE_URL: null,
DEBUG_MODE: null, DEBUG_MODE: null,
STUN_SERVER: null, STUN_SERVER: null,
TURN_SERVER: null, TURN_SERVER: null,

View file

@ -4210,9 +4210,9 @@ path-key@^3.0.0, path-key@^3.1.0:
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6: path-parse@^1.0.6:
version "1.0.6" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.7: path-to-regexp@0.1.7:
version "0.1.7" version "0.1.7"
@ -5745,9 +5745,9 @@ urix@^0.1.0:
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-parse@^1.4.3, url-parse@^1.5.1: url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.1" version "1.5.3"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
dependencies: dependencies:
querystringify "^2.1.1" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.0.0"

View file

@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<title>Custom Iframe Menu</title>
</head>
<body style="text-align: center">
<p style="color: whitesmoke">This is an iframe in a custom menu.</p>
</body>
</html>

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