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
### 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
- New scripting API features :
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.

View file

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

View file

@ -40,7 +40,7 @@
},
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
"dependencies": {
"@workadventure/tiled-map-type-guard": "^1.0.0",
"@workadventure/tiled-map-type-guard": "^1.0.2",
"axios": "^0.21.1",
"busboy": "^0.3.1",
"circular-json": "^0.5.9",
@ -54,7 +54,6 @@
"prom-client": "^12.0.0",
"query-string": "^6.13.3",
"redis": "^3.1.2",
"systeminformation": "^4.31.1",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
"uuidv4": "^6.0.7"
},

View file

@ -1,7 +1,12 @@
/**
* Handles variables shared between the scripting API and the server.
*/
import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist";
import {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapObjectLayer,
} from "@workadventure/tiled-map-type-guard/dist";
import { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient";
@ -83,25 +88,33 @@ export class VariablesManager {
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
const objects = new Map<string, Variable>();
for (const layer of map.layers) {
if (layer.type === "objectgroup") {
for (const object of (layer as ITiledMapObjectLayer).objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
continue;
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
this.recursiveFindVariablesInLayer(layer, objects);
}
return objects;
}
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
continue;
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
} else if (layer.type === "group") {
for (const innerLayer of layer.layers) {
this.recursiveFindVariablesInLayer(innerLayer, objects);
}
}
}
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = {};

View file

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

View file

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

View file

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

View file

@ -101,6 +101,7 @@
},
"redis": {
"image": "redis:6",
"ports": [6379]
}
},
"config": {

View file

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

View file

@ -9,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.loadVariable(key : string) : unknown
WA.state.hasVariable(key : string) : boolean
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
WA.state.[any property]: unknown
```

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore";
class ConnectionManager {
private localUser!: LocalUser;
@ -15,6 +17,7 @@ class ConnectionManager {
private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false;
private authToken: string | null = null;
private _currentRoom: Room | null = null;
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 nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null);
//TODO refactor this and don't realise previous call
return Axios.get(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`)
.then(() => {
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
})
.catch((err) => {
console.error(err, "We don't have URL to regenerate authentication user");
//TODO show modal login
window.location.reload();
});
//TODO fix me to redirect this URL by pusher
if (!this._currentRoom || !this._currentRoom.iframeAuthentication) {
loginSceneVisibleIframeStore.set(false);
return null;
}
const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}`;
window.location.assign(redirectUrl);
return redirectUrl;
}
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);
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> {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
let room: Room | null = null;
if (connexionType === GameConnexionTypes.jwt) {
this._currentRoom = null;
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 code = urlParams.get("code");
const state = urlParams.get("state");
@ -71,14 +90,15 @@ class ConnectionManager {
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(room);
localUserStore.setCode(code);
try {
await this.checkAuthUserConnexion();
} catch (err) {
console.error(err);
this.loadOpenIDScreen();
}
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.register) {
//@deprecated
const organizationMemberToken = urlManager.getOrganizationToken();
@ -92,7 +112,7 @@ class ConnectionManager {
const roomUrl = data.roomUrl;
room = await Room.createRoom(
this._currentRoom = await Room.createRoom(
new URL(
window.location.protocol +
"//" +
@ -102,7 +122,7 @@ class ConnectionManager {
window.location.hash
)
);
urlManager.pushRoomIdToUrl(room);
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (
connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous ||
@ -112,12 +132,18 @@ class ConnectionManager {
//todo: add here some kind of warning if authToken has expired.
if (!this.authToken) {
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
let roomPath: string;
if (connexionType === GameConnexionTypes.empty) {
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
roomPath = localUserStore.getLastRoomUrl();
//get last room path from cache api
try {
const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi();
@ -138,13 +164,13 @@ class ConnectionManager {
}
//get detail map for anonymous login and set texture in local storage
room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) {
this._currentRoom = await Room.createRoom(new URL(roomPath));
if (this._currentRoom.textures != undefined && this._currentRoom.textures.length > 0) {
//check if texture was changed
if (this.localUser.textures.length === 0) {
this.localUser.textures = room.textures;
this.localUser.textures = this._currentRoom.textures;
} else {
room.textures.forEach((newTexture) => {
this._currentRoom.textures.forEach((newTexture) => {
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return;
@ -155,12 +181,12 @@ class ConnectionManager {
localUserStore.saveUser(this.localUser);
}
}
if (room == undefined) {
if (this._currentRoom == undefined) {
return Promise.reject(new Error("Invalid URL"));
}
this.serviceWorker = new _ServiceWorker();
return Promise.resolve(room);
return Promise.resolve(this._currentRoom);
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
@ -215,9 +241,6 @@ class ConnectionManager {
});
connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect);
});
}).catch((err) => {
@ -237,6 +260,34 @@ class ConnectionManager {
get getConnexionType() {
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();

View file

@ -1,5 +1,6 @@
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid";
import { START_ROOM_URL } from "../Enum/EnvironmentVariable";
const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer";
@ -16,6 +17,9 @@ const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
const nonce = "nonce";
const notification = "notificationPermission";
const code = "code";
const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache";
@ -124,7 +128,9 @@ class LocalUserStore {
});
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
}
getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => {
@ -143,6 +149,14 @@ class LocalUserStore {
return localStorage.getItem(authToken);
}
setNotification(value: string): void {
localStorage.setItem(notification, value);
}
getNotification(): string | null {
return localStorage.getItem(notification);
}
generateState(): string {
const newState = uuidv4();
localStorage.setItem(state, newState);
@ -151,19 +165,32 @@ class LocalUserStore {
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value;
}
getState(): string | null {
return localStorage.getItem(state);
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
const oldValue = localStorage.getItem(nonce);
localStorage.removeItem(nonce);
return oldValue;
return localStorage.getItem(nonce);
}
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 {
public readonly id: string;
public readonly isPublic: boolean;
private _authenticationMandatory: boolean = false;
private _iframeAuthentication?: string;
private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined;
private instance: string | undefined;
@ -101,6 +103,8 @@ export class Room {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
this._authenticationMandatory = data.authenticationMandatory || false;
this._iframeAuthentication = data.iframeAuthentication;
return new MapDetail(data.mapUrl, data.textures);
}
@ -186,4 +190,12 @@ export class Room {
}
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 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(
token: string | null,
@ -218,7 +223,7 @@ export class RoomConnection implements RoomConnection {
worldFullMessageStream.onMessage();
this.closed = true;
} 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
} else if (message.hasWorldconnexionmessage()) {
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 DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
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;

View file

@ -1,48 +1,63 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
import { DirtyScene } from "../Game/DirtyScene";
const LogoNameIndex: string = 'logoLoading';
const TextName: string = 'Loading...';
const LogoResource: string = 'resources/logos/logo.png';
const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59};
const LogoNameIndex: string = "logoLoading";
const TextName: string = "Loading...";
const LogoResource: string = "resources/logos/logo.png";
const LogoFrame: ImageFrameConfig = { frameWidth: 307, frameHeight: 59 };
export const addLoader = (scene: Phaser.Scene): void => {
// If there is nothing to load, do not display the loader.
if (scene.load.list.entries.length === 0) {
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 loadingBarHeight: number = 16;
const padding: number = 5;
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
if(scene.load.textureManager.exists(LogoNameIndex)){
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
}else{
if (scene.load.textureManager.exists(LogoNameIndex)) {
return res(
scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)
);
} else {
//add loading if logo image is not ready
loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName);
}
scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if(loadingText){
if (loadingText) {
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 progress = scene.add.graphics();
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.fillStyle(0xBBBBBB, 1);
progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight);
progress.fillStyle(0xbbbbbb, 1);
progress.fillRect(
(scene.game.renderer.width - loadingBarWidth) / 2,
scene.game.renderer.height / 2 + 50,
loadingBarWidth * value,
loadingBarHeight
);
});
scene.load.on('complete', () => {
if(loadingText){
scene.load.on("complete", () => {
if (loadingText) {
loadingText.destroy();
}
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
@ -50,5 +65,14 @@ export const addLoader = (scene: Phaser.Scene): void => {
});
progress.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 {SpeechBubble} from "./SpeechBubble";
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
import { SpeechBubble } from "./SpeechBubble";
import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite;
import {TextureError} from "../../Exception/TextureError";
import {Companion} from "../Companion/Companion";
import type {GameScene} from "../Game/GameScene";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
import { TextureError } from "../../Exception/TextureError";
import { Companion } from "../Companion/Companion";
import type { GameScene } from "../Game/GameScene";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import { waScaleManager } from "../Services/WaScaleManager";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { isSilentStore } from "../../Stores/MediaStore";
const playerNameY = - 25;
const playerNameY = -25;
interface AnimationData {
key: string;
frameRate: number;
repeat: number;
frameModel: string; //todo use an enum
frames : number[]
frames: number[];
}
const interactiveRadius = 35;
export abstract class Character extends Container {
private bubble: SpeechBubble|null = null;
private bubble: SpeechBubble | null = null;
private readonly playerName: Text;
public PlayerValue: string;
public sprites: Map<string, Sprite>;
@ -32,35 +33,41 @@ export abstract class Character extends Container {
private invisible: boolean;
public companion?: Companion;
private emote: Phaser.GameObjects.Sprite | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null;
private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene;
constructor(scene: GameScene,
x: number,
y: number,
texturesPromise: Promise<string[]>,
name: string,
direction: PlayerAnimationDirections,
moving: boolean,
frame: string | number,
isClickable: boolean,
companion: string|null,
companionTexturePromise?: Promise<string>
constructor(
scene: GameScene,
x: number,
y: number,
texturesPromise: Promise<string[]>,
name: string,
direction: PlayerAnimationDirections,
moving: boolean,
frame: string | number,
isClickable: boolean,
companion: string | null,
companionTexturePromise?: Promise<string>
) {
super(scene, x, y/*, texture, frame*/);
super(scene, x, y /*, texture, frame*/);
this.scene = scene;
this.PlayerValue = name;
this.invisible = true
this.invisible = true;
this.sprites = new Map<string, Sprite>();
//textures are inside a Promise in case they need to be lazyloaded before use.
texturesPromise.then((textures) => {
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.add(this.playerName);
@ -71,18 +78,17 @@ export abstract class Character extends Container {
useHandCursor: true,
});
this.on('pointerover',() => {
this.on("pointerover", () => {
this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2,
outlineColor: 0xffff00
outlineColor: 0xffff00,
});
this.scene.markDirty();
});
this.on('pointerout',() => {
this.on("pointerout", () => {
this.getOutlinePlugin()?.remove(this.playerName);
this.scene.markDirty();
})
});
}
scene.add.existing(this);
@ -97,38 +103,38 @@ export abstract class Character extends Container {
this.playAnimation(direction, moving);
if (typeof companion === 'string') {
if (typeof companion === "string") {
this.addCompanion(companion, companionTexturePromise);
}
}
private getOutlinePlugin(): OutlinePipelinePlugin|undefined {
return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined;
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
}
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);
}
}
public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) {
if(this.scene && !this.scene.textures.exists(texture)){
throw new TextureError('texture not found');
if (this.scene && !this.scene.textures.exists(texture)) {
throw new TextureError("texture not found");
}
const sprite = new Sprite(this.scene, 0, 0, texture, frame);
this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => {
this.getPlayerAnimations(texture).forEach((d) => {
this.scene.anims.create({
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,
repeat: d.repeat
repeat: d.repeat,
});
})
});
// Needed, otherwise, animations are not handled correctly.
if(this.scene) {
if (this.scene) {
this.scene.sys.updateList.add(sprite);
}
this.sprites.set(texture, sprite);
@ -136,68 +142,77 @@ export abstract class Character extends Container {
}
private getPlayerAnimations(name: string): AnimationData[] {
return [{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [0, 1, 2, 1],
frameRate: 10,
repeat: -1
}, {
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [3, 4, 5, 4],
frameRate: 10,
repeat: -1
}, {
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [6, 7, 8, 7],
frameRate: 10,
repeat: -1
}, {
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [9, 10, 11, 10],
frameRate: 10,
repeat: -1
},{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [1],
frameRate: 10,
repeat: 1
}, {
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [4],
frameRate: 10,
repeat: 1
}, {
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [7],
frameRate: 10,
repeat: 1
}, {
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [10],
frameRate: 10,
repeat: 1
}];
return [
{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [0, 1, 2, 1],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [3, 4, 5, 4],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [6, 7, 8, 7],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [9, 10, 11, 10],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [1],
frameRate: 10,
repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [4],
frameRate: 10,
repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [7],
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;
for (const [texture, sprite] of this.sprites.entries()) {
if (!sprite.anims) {
console.error('ANIMS IS NOT DEFINED!!!');
console.error("ANIMS IS NOT DEFINED!!!");
return;
}
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) {
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 {
const body = this.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;
}
@ -216,16 +231,20 @@ export abstract class Character extends Container {
body.setVelocity(x, y);
// 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.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.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.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.playAnimation(PlayerAnimationDirections.Left, true);
}
@ -237,32 +256,39 @@ export abstract class Character extends Container {
}
}
stop(){
stop() {
this.getBody().setVelocity(0, 0);
this.playAnimation(this.lastDirection, false);
}
say(text: string) {
if (this.bubble) return;
this.bubble = new SpeechBubble(this.scene, this, text)
this.bubble = new SpeechBubble(this.scene, this, text);
setTimeout(() => {
if (this.bubble !== null) {
this.bubble.destroy();
this.bubble = null;
}
}, 3000)
}, 3000);
}
destroy(): void {
for (const sprite of this.sprites.values()) {
if(this.scene) {
if (this.scene) {
this.scene.sys.updateList.remove(sprite);
}
}
this.list.forEach(objectContaining => objectContaining.destroy())
this.list.forEach((objectContaining) => objectContaining.destroy());
super.destroy();
}
isSilent() {
isSilentStore.set(true);
}
noSilent() {
isSilentStore.set(false);
}
playEmote(emoteKey: string) {
this.cancelPreviousEmote();
@ -270,7 +296,7 @@ export abstract class Character extends Container {
const emoteY = -30 - scalingFactor * 10;
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.setScale(0.1 * scalingFactor);
this.add(this.emote);
@ -287,11 +313,11 @@ export abstract class Character extends Container {
alpha: 1,
y: emoteY,
},
ease: 'Power2',
ease: "Power2",
duration: 500,
onComplete: () => {
this.startPulseTransition(emoteY, scalingFactor);
}
},
});
}
@ -300,7 +326,7 @@ export abstract class Character extends Container {
targets: this.emote,
props: {
y: emoteY * 1.3,
scale: scalingFactor * 1.1
scale: scalingFactor * 1.1,
},
duration: 250,
yoyo: true,
@ -308,7 +334,7 @@ export abstract class Character extends Container {
completeDelay: 200,
onComplete: () => {
this.startExitTransition(emoteY);
}
},
});
}
@ -319,11 +345,11 @@ export abstract class Character extends Container {
alpha: 0,
y: 2 * emoteY,
},
ease: 'Power2',
ease: "Power2",
duration: 500,
onComplete: () => {
this.destroyEmote();
}
},
});
}
@ -331,7 +357,7 @@ export abstract class Character extends Container {
if (!this.emote) return;
this.emoteTween?.remove();
this.destroyEmote()
this.destroyEmote();
}
private destroyEmote() {

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import { GameScene } from "./GameScene";
import { connectionManager } from "../../Connexion/ConnectionManager";
import type { Room } from "../../Connexion/Room";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import { LoginSceneName } from "../Login/LoginScene";
import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { EnableCameraSceneName } from "../Login/EnableCameraScene";
@ -9,6 +8,7 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store";
import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import { menuIconVisiblilityStore } from "../../Stores/MenuStore";
/**
* This class should be responsible for any scene starting/stopping
@ -18,24 +18,34 @@ export class GameManager {
private characterLayers: string[] | null;
private companion: string | null;
private startRoom!: Room;
private cameraSetup?: { video: unknown; audio: unknown };
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() {
this.playerName = localUserStore.getName();
this.characterLayers = localUserStore.getCharacterLayers();
this.companion = localUserStore.getCompanion();
this.cameraSetup = localUserStore.getCameraSetup();
}
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
this.scenePlugin = scenePlugin;
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;
} else if (!this.characterLayers || !this.characterLayers.length) {
return SelectCharacterSceneName;
} else {
} else if (this.cameraSetup == undefined) {
return EnableCameraSceneName;
} else {
this.activeMenuSceneAndHelpCameraSettings();
return this.startRoom.key;
}
}
@ -68,21 +78,27 @@ export class GameManager {
return this.companion;
}
public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
public loadMap(room: Room) {
const roomID = room.key;
const gameIndex = scenePlugin.getIndex(roomID);
const gameIndex = this.scenePlugin.getIndex(roomID);
if (gameIndex === -1) {
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));
scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
scenePlugin.launch(MenuSceneName);
this.scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
this.activeMenuSceneAndHelpCameraSettings();
}
/**
* @private
* @return void
*/
private activeMenuSceneAndHelpCameraSettings(): void {
if (
!localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState))
@ -94,41 +110,44 @@ export class GameManager {
public gameSceneIsCreated(scene: GameScene) {
this.currentGameSceneName = scene.scene.key;
const menuScene: MenuScene = scene.scene.get(MenuSceneName) as MenuScene;
menuScene.revealMenuIcon();
menuIconVisiblilityStore.set(true);
}
/**
* 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.
*/
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!";
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
const gameScene: GameScene = this.scenePlugin.get(this.currentGameSceneName) as GameScene;
gameScene.cleanupClosingScene();
scene.scene.stop(this.currentGameSceneName);
scene.scene.sleep(MenuSceneName);
if (!scene.scene.get(targetSceneName)) {
scene.scene.add(targetSceneName, sceneClass, false);
gameScene.createSuccessorGameScene(false, false);
menuIconVisiblilityStore.set(false);
if (!this.scenePlugin.get(targetSceneName)) {
this.scenePlugin.add(targetSceneName, sceneClass, false);
}
scene.scene.run(targetSceneName);
this.scenePlugin.run(targetSceneName);
}
/**
* follow up to leaveGame()
*/
tryResumingGame(scene: Phaser.Scene, fallbackSceneName: string) {
tryResumingGame(fallbackSceneName: string) {
if (this.currentGameSceneName) {
scene.scene.start(this.currentGameSceneName);
scene.scene.wake(MenuSceneName);
this.scenePlugin.start(this.currentGameSceneName);
menuIconVisiblilityStore.set(true);
} 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!";
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;
}
this.key = key;
this.triggerAll();
}
const newProps = this.getProperties(key);
private triggerAll(): void {
const newProps = this.getProperties(this.key ?? 0);
const oldProps = this.lastProperties;
this.lastProperties = newProps;
@ -253,4 +256,34 @@ export class GameMap {
}
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 { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { addLoader } from "../Components/Loader";
import { addLoader, removeLoader } from "../Components/Loader";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer";
@ -45,7 +45,6 @@ import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
@ -92,9 +91,9 @@ import { PropertyUtils } from "../Map/PropertyUtils";
import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { get } from "svelte/store";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import type { RadialMenuItem } from "../Components/RadialMenu";
export interface GameSceneInitInterface {
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.
console.error("Error when loading: ", file);
if (this.preloading) {
//remove loader in progress
removeLoader(this);
//display an error scene
this.scene.start(ErrorSceneName, {
title: "Network error",
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
@ -569,6 +565,12 @@ export class GameScene extends DirtyScene {
}
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.outlinedItem?.activate();
@ -583,9 +585,11 @@ export class GameScene extends DirtyScene {
this.updateCameraOffset(box)
);
new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) {
this.scene.sleep();
this.connect();
}
@ -609,6 +613,10 @@ export class GameScene extends DirtyScene {
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((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(() => {
console.log("Player disconnected from server. Reloading scene.");
this.cleanupClosingScene();
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.createSuccessorGameScene(true, true);
});
this.connection.onActionableEvent((message) => {
@ -722,26 +718,9 @@ export class GameScene extends DirtyScene {
});
// When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer);
this.simplePeer = new SimplePeer(this.connection);
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
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this));
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this));
@ -760,8 +739,9 @@ export class GameScene extends DirtyScene {
this.connectionAnswerPromiseResolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console.
this.scene.wake();
this.scene.stop(ReconnectingSceneName);
if (this.scene.isSleeping()) {
this.scene.stop(ReconnectingSceneName);
}
//init user position and play trigger to check layers properties
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
@ -834,39 +814,7 @@ export class GameScene extends DirtyScene {
}, 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) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
@ -906,8 +854,10 @@ export class GameScene extends DirtyScene {
this.gameMap.onPropertyChange("silent", (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.connection?.setSilent(false);
this.CurrentPlayer.noSilent();
} else {
this.connection?.setSilent(true);
this.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => {
@ -927,9 +877,10 @@ export class GameScene extends DirtyScene {
});
this.gameMap.onPropertyChange("zone", (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
} else {
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
@ -952,9 +903,13 @@ export class GameScene extends DirtyScene {
return;
}
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}
</div> `;
}
const buttonContainer = '<div class="buttonContainer"</div>';
html += buttonContainer;
let id = 0;
@ -984,7 +939,11 @@ ${escapedMessage}
const btnId = id;
button.onclick = () => {
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;
setTimeout(() => {
button.disabled = false;
}, 100);
};
id++;
}
@ -1240,30 +1199,10 @@ ${escapedMessage}
propertyName: string,
propertyValue: string | number | boolean | undefined
): 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") {
this.loadNextGameFromExitUrl(propertyValue);
}
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.gameMap.setLayerProperty(layerName, propertyName, propertyValue);
}
private setLayerVisibility(layerName: string, visible: boolean): void {
@ -1322,13 +1261,12 @@ ${escapedMessage}
urlManager.pushStartLayerNameToUrl(roomUrl.hash);
}
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
menuScene.reset();
if (!targetRoom.isEqual(this.room)) {
if (this.scene.get(targetRoom.key) === null) {
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.scene.stop();
@ -1368,7 +1306,6 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset");
iframeListener.unregisterAnswerer("getMapData");
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("triggerActionMessage");
iframeListener.unregisterAnswerer("removeActionMessage");
this.sharedVariablesManager?.close();
@ -1443,7 +1380,7 @@ ${escapedMessage}
private async loadNextGame(exitRoomPath: URL): Promise<void> {
try {
const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene);
return gameManager.loadMap(room);
} catch (e /*: unknown*/) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
}
@ -1504,7 +1441,7 @@ ${escapedMessage}
});
} catch (err) {
if (err instanceof TextureError) {
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
}
throw err;
}
@ -1863,8 +1800,9 @@ ${escapedMessage}
"jitsiInterfaceConfig"
);
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);
mediaManager.hideGameOverlay();
@ -1920,4 +1858,24 @@ ${escapedMessage}
waScaleManager.zoomModifier *= zoomFactor;
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 { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { GameMap } from "./GameMap";
import type { ITile, ITiledMapObject } from "../Map/ITiledMap";
import type { Var } from "svelte/types/compiler/interfaces";
import { init } from "svelte/internal";
import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap";
interface Variable {
defaultValue: unknown;
@ -100,24 +97,33 @@ export class SharedVariablesManager {
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
const objects = new Map<string, Variable>();
for (const layer of gameMap.getMap().layers) {
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.'
);
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
this.recursiveFindVariablesInLayer(layer, objects);
}
return objects;
}
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
continue;
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
} else if (layer.type === "group") {
for (const innerLayer of layer.layers) {
this.recursiveFindVariablesInLayer(innerLayer, objects);
}
}
}
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = {
defaultValue: undefined,

View file

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

View file

@ -1,50 +1,35 @@
import {gameManager} from "../Game/GameManager";
import {TextField} from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import {mediaManager} from "../../WebRtc/MediaManager";
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";
import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene";
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
export const EnableCameraSceneName = "EnableCameraScene";
export class EnableCameraScene extends ResizableScene {
constructor() {
super({
key: EnableCameraSceneName
key: EnableCameraSceneName,
});
}
preload() {
}
preload() {}
create() {
this.input.keyboard.on('keyup-ENTER', () => {
this.input.keyboard.on("keyup-ENTER", () => {
this.login();
});
enableCameraSceneVisibilityStore.showEnableCameraScene();
}
public onResize(): void {
}
public onResize(): void {}
update(time: number, delta: number): void {
}
update(time: number, delta: number): void {}
public login(): void {
enableCameraSceneVisibilityStore.hideEnableCameraScene();
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 {ResizableScene} from "./ResizableScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import { SelectCharacterSceneName } from "./SelectCharacterScene";
import { ResizableScene } from "./ResizableScene";
import { loginSceneVisibleIframeStore, loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { gameManager } from "../Game/GameManager";
export const LoginSceneName = "LoginScene";
export class LoginScene extends ResizableScene {
private name: string = '';
private name: string = "";
constructor() {
super({
key: LoginSceneName
key: LoginSceneName,
});
this.name = gameManager.getPlayerName() || '';
this.name = gameManager.getPlayerName() || "";
}
preload() {
}
preload() {}
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);
}
@ -27,15 +37,13 @@ export class LoginScene extends ResizableScene {
name = name.trim();
gameManager.setPlayerName(name);
this.scene.stop(LoginSceneName)
gameManager.tryResumingGame(this, SelectCharacterSceneName);
this.scene.stop(LoginSceneName);
gameManager.tryResumingGame(SelectCharacterSceneName);
this.scene.remove(LoginSceneName);
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);
waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(this, EnableCameraSceneName);
gameManager.tryResumingGame(EnableCameraSceneName);
this.players = [];
selectCharacterSceneVisibleStore.set(false);
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 { gameManager} from "../Game/GameManager";
import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene";
import { EnableCameraSceneName } from "./EnableCameraScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import type { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures";
import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingManager";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import { MenuScene } from "../Menu/MenuScene";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
export const SelectCompanionSceneName = "SelectCompanionScene";
@ -28,12 +25,12 @@ export class SelectCompanionScene extends ResizableScene {
constructor() {
super({
key: SelectCompanionSceneName
key: SelectCompanionSceneName,
});
}
preload() {
getAllCompanionResources(this.load).forEach(model => {
getAllCompanionResources(this.load).forEach((model) => {
this.companionModels.push(model);
});
@ -42,7 +39,6 @@ export class SelectCompanionScene extends ResizableScene {
}
create() {
selectCompanionSceneVisibleStore.set(true);
waScaleManager.saveZoom();
@ -53,14 +49,16 @@ export class SelectCompanionScene extends ResizableScene {
}
// 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-LEFT', this.moveToLeft.bind(this));
this.input.keyboard.on("keydown-RIGHT", this.moveToRight.bind(this));
this.input.keyboard.on("keydown-LEFT", this.moveToLeft.bind(this));
if(localUserStore.getCompanion()){
const companionIndex = this.companionModels.findIndex((companion) => companion.name === localUserStore.getCompanion());
if(companionIndex > -1 || companionIndex < this.companions.length){
if (localUserStore.getCompanion()) {
const companionIndex = this.companionModels.findIndex(
(companion) => companion.name === localUserStore.getCompanion()
);
if (companionIndex > -1 || companionIndex < this.companions.length) {
this.currentCompanion = companionIndex;
this.selectedCompanion = this.companions[companionIndex];
}
@ -89,26 +87,26 @@ export class SelectCompanionScene extends ResizableScene {
this.closeScene();
}
public closeScene(){
public closeScene() {
// next scene
this.scene.stop(SelectCompanionSceneName);
waScaleManager.restoreZoom();
gameManager.tryResumingGame(this, EnableCameraSceneName);
gameManager.tryResumingGame(EnableCameraSceneName);
this.scene.remove(SelectCompanionSceneName);
selectCompanionSceneVisibleStore.set(false);
}
private createCurrentCompanion(): void {
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 companion = this.physics.add.sprite(middleX, middleY, companionResource.name, 0);
this.setUpCompanion(companion, i);
this.anims.create({
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,
repeat: -1
repeat: -1,
});
companion.setInteractive().on("pointerdown", () => {
@ -140,87 +138,84 @@ export class SelectCompanionScene extends ResizableScene {
this.selectedCompanion = companion;
}
private moveCompanion(){
for(let i = 0; i < this.companions.length; i++){
private moveCompanion() {
for (let i = 0; i < this.companions.length; i++) {
const companion = this.companions[i];
this.setUpCompanion(companion, i);
}
this.updateSelectedCompanion();
}
public moveToRight(){
if(this.currentCompanion === (this.companions.length - 1)){
public moveToRight() {
if (this.currentCompanion === this.companions.length - 1) {
return;
}
this.currentCompanion += 1;
this.moveCompanion();
}
public moveToLeft(){
if(this.currentCompanion === 0){
public moveToLeft() {
if (this.currentCompanion === 0) {
return;
}
this.currentCompanion -= 1;
this.moveCompanion();
}
private defineSetupCompanion(num: number){
private defineSetupCompanion(num: number) {
const deltaX = 30;
const deltaY = 2;
let [companionX, companionY] = this.getCompanionPosition();
let companionVisible = true;
let companionScale = 1.5;
let companionOpactity = 1;
if( this.currentCompanion !== num ){
if (this.currentCompanion !== num) {
companionVisible = false;
}
if( num === (this.currentCompanion + 1) ){
if (num === this.currentCompanion + 1) {
companionY -= deltaY;
companionX += deltaX;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
if( num === (this.currentCompanion + 2) ){
if (num === this.currentCompanion + 2) {
companionY -= deltaY;
companionX += (deltaX * 2);
companionX += deltaX * 2;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
if( num === (this.currentCompanion - 1) ){
if (num === this.currentCompanion - 1) {
companionY -= deltaY;
companionX -= deltaX;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
if( num === (this.currentCompanion - 2) ){
if (num === this.currentCompanion - 2) {
companionY -= deltaY;
companionX -= (deltaX * 2);
companionX -= deltaX * 2;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
return {companionX, companionY, companionScale, companionOpactity, companionVisible}
return { companionX, companionY, companionScale, companionOpactity, companionVisible };
}
/**
* Returns pixel position by on column and row number
*/
private getCompanionPosition(): [number, number] {
return [
this.game.renderer.width / 2,
this.game.renderer.height / 3
];
return [this.game.renderer.width / 2, this.game.renderer.height / 3];
}
private setUpCompanion(companion: Phaser.Physics.Arcade.Sprite, numero: number){
const {companionX, companionY, companionScale, companionOpactity, companionVisible} = this.defineSetupCompanion(numero);
private setUpCompanion(companion: Phaser.Physics.Arcade.Sprite, numero: number) {
const { companionX, companionY, companionScale, companionOpactity, companionVisible } =
this.defineSetupCompanion(numero);
companion.setBounce(0.2);
companion.setCollideWorldBounds(true);
companion.setVisible( companionVisible );
companion.setVisible(companionVisible);
companion.setScale(companionScale, companionScale);
companion.setAlpha(companionOpactity);
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 type {GameScene} from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character";
import {userMovingStore} from "../../Stores/GameStore";
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
import { PlayerAnimationDirections } from "./Animation";
import type { GameScene } from "../Game/GameScene";
import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character";
import { userMovingStore } from "../../Stores/GameStore";
import { RadialMenu, RadialMenuClickEvent, RadialMenuItem } from "../Components/RadialMenu";
export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote";
@ -11,7 +11,7 @@ export const requestEmoteEventName = "requestEmote";
export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false;
private emoteMenu: RadialMenu|null = null;
private emoteMenu: RadialMenu | null = null;
private updateListener: () => void;
constructor(
@ -23,7 +23,7 @@ export class Player extends Character {
direction: PlayerAnimationDirections,
moving: boolean,
private userInputManager: UserInputManager,
companion: string|null,
companion: string | null,
companionTexturePromise?: Promise<string>
) {
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.scene.events.addListener('postupdate', this.updateListener);
this.scene.events.addListener("postupdate", this.updateListener);
}
moveUser(delta: number): void {
@ -73,14 +73,14 @@ export class Player extends Character {
if (x !== 0 || y !== 0) {
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) {
// slow joystick movement
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) {
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) {
@ -94,17 +94,17 @@ export class Player extends Character {
return this.wasMoving;
}
openOrCloseEmoteMenu(emotes:RadialMenuItem[]) {
if(this.emoteMenu) {
openOrCloseEmoteMenu(emotes: RadialMenuItem[]) {
if (this.emoteMenu) {
this.closeEmoteMenu();
} else {
this.openEmoteMenu(emotes);
}
}
openEmoteMenu(emotes:RadialMenuItem[]): void {
openEmoteMenu(emotes: RadialMenuItem[]): void {
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.closeEmoteMenu();
this.emit(requestEmoteEventName, item.name);
@ -112,6 +112,13 @@ export class Player extends Character {
});
}
isSilent() {
super.isSilent();
}
noSilent() {
super.noSilent();
}
closeEmoteMenu(): void {
if (!this.emoteMenu) return;
this.emoteMenu.destroy();
@ -119,7 +126,7 @@ export class Player extends Character {
}
destroy() {
this.scene.events.removeListener('postupdate', this.updateListener);
this.scene.events.removeListener("postupdate", this.updateListener);
super.destroy();
}
}

View file

@ -1,4 +1,5 @@
import { get, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
export interface audioManagerVolume {
muted: boolean;
@ -103,3 +104,7 @@ export const audioManagerVisibilityStore = writable(false);
export const audioManagerVolumeStore = createAudioManagerVolumeStore();
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";
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 { userMovingStore } from "./GameStore";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
@ -9,6 +9,7 @@ import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
/**
* A store that contains the camera state requested by the user (on or off).
@ -170,7 +171,7 @@ function createVideoConstraintStore() {
setFrameRate: (frameRate: number) =>
update((constraints) => {
constraints.frameRate = { ideal: frameRate };
localUserStore.setVideoQualityValue(frameRate);
return constraints;
}),
};
@ -255,6 +256,12 @@ export const mediaStreamConstraintsStore = derived(
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
localUserStore.setCameraSetup(
JSON.stringify({
video: currentVideoConstraint,
audio: currentAudioConstraint,
})
);
return;
}
@ -324,14 +331,11 @@ export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
interface StreamSuccessValue {
type: "success";
stream: MediaStream | null;
// The constraints that we got (and not the one that have been requested)
constraints: MediaStreamConstraints;
}
interface StreamErrorValue {
type: "error";
error: Error;
constraints: MediaStreamConstraints;
}
let currentStream: MediaStream | null = null;
@ -339,10 +343,13 @@ let currentStream: MediaStream | null = null;
/**
* Stops the camera from filming
*/
function stopCamera(): void {
function applyCameraConstraints(currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean): void {
if (currentStream) {
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
*/
function stopMicrophone(): void {
function applyMicrophoneConstraints(
currentStream: MediaStream | null,
constraints: MediaTrackConstraints | boolean
): void {
if (currentStream) {
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({
type: "error",
error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."),
constraints,
});
return;
} else if (isIOS()) {
set({
type: "error",
error: new WebviewOnOldIOS(),
constraints,
});
return;
} else {
set({
type: "error",
error: new BrowserTooOldError(),
constraints,
});
return;
}
}
if (constraints.audio === false) {
stopMicrophone();
}
if (constraints.video === false) {
stopCamera();
}
applyMicrophoneConstraints(currentStream, constraints.audio || false);
applyCameraConstraints(currentStream, constraints.video || false);
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
if (currentStream === null) {
// we need to assign a first value to the stream because getUserMedia is async
set({
type: "success",
stream: null,
constraints,
});
return;
}
(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);
(async () => {
try {
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'success',
type: "success",
stream: currentStream,
constraints
});
return;
} catch (e2) {
console.info("Error. Unable to get microphone fallback access.", $mediaStreamConstraintsStore, e2);
set({
type: 'error',
error: e,
constraints
});
} catch (e) {
if (constraints.video !== false || constraints.audio !== 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,
});
// 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) => {
return $localStreamStore.constraints;
});
export const obtainedMediaConstraintStore = derived<Readable<MediaStreamConstraints>, ObtainedMediaStreamConstraints>(
mediaStreamConstraintsStore,
($mediaStreamConstraintsStore, set) => {
const newObtainedMediaConstraint = {
video: !!$mediaStreamConstraintsStore.video,
audio: !!$mediaStreamConstraintsStore.audio,
};
if (newObtainedMediaConstraint !== obtainedMediaConstraint) {
obtainedMediaConstraint = newObtainedMediaConstraint;
set(obtainedMediaConstraint);
}
}
);
/**
* Device list
@ -581,3 +578,8 @@ localStreamStore.subscribe((streamResult) => {
* A store containing the real active media is mobile
*/
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 { 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;
function createWarningContainerStore() {
@ -21,3 +27,90 @@ function 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 type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import { VideoPeer } from "../WebRtc/VideoPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import type { VideoPeer } from "../WebRtc/VideoPeer";
import type { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
/**
* A store that contains the list of (video) peers we are connected to.
*/
function createPeerStore() {
let peers = new Map<number, VideoPeer>();
const { subscribe, set, update } = writable(peers);
const { subscribe, set, update } = writable(new Map<number, VideoPeer>());
return {
subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => {
peers = new Map<number, VideoPeer>();
set(peers);
simplePeer.registerPeerConnectionListener({
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;
});
},
pushNewPeer(peer: VideoPeer) {
update((users) => {
users.set(peer.userId, peer);
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.
*/
function createScreenSharingPeerStore() {
let peers = new Map<number, ScreenSharingPeer>();
const { subscribe, set, update } = writable(peers);
const { subscribe, set, update } = writable(new Map<number, ScreenSharingPeer>());
return {
subscribe,
connectToSimplePeer: (simplePeer: SimplePeer) => {
peers = new Map<number, ScreenSharingPeer>();
set(peers);
simplePeer.registerPeerConnectionListener({
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;
});
},
pushNewPeer(peer: ScreenSharingPeer) {
update((users) => {
users.set(peer.userId, peer);
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 { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator";
import { localUserStore } from "../Connexion/LocalUserStore";
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 type { LocalStreamStoreValue } from "./MediaStore";
import { DivImportance } from "../WebRtc/LayoutManager";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -106,7 +105,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({
type: "success",
stream: null,
constraints,
});
return;
}
@ -121,7 +119,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({
type: "error",
error: new Error("Your browser does not support sharing screen"),
constraints,
});
return;
}
@ -141,10 +138,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({
type: "success",
stream: null,
constraints: {
video: false,
audio: false,
},
});
};
}
@ -152,7 +145,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({
type: "success",
stream: currentStream,
constraints,
});
return;
} catch (e) {
@ -162,7 +154,6 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
set({
type: "error",
error: e,
constraints,
});
}
})();
@ -184,7 +175,7 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
export interface ScreenSharingLocalMedia {
uniqueId: string;
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";
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 { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
import { menuInputFocusStore } from "./MenuStore";
import { chatInputFocusStore } from "./ChatStore";
import { showReportScreenStore, userReportEmpty } from "./ShowReportScreenStore";
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
export const enableUserInputsStore = derived(
[consoleGlobalMessageManagerFocusStore, chatInputFocusStore],
([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => {
return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore;
[menuInputFocusStore, chatInputFocusStore, showReportScreenStore],
([$menuInputFocusStore, $chatInputFocusStore, $showReportScreenStore]) => {
return !$menuInputFocusStore && !$chatInputFocusStore && !($showReportScreenStore !== userReportEmpty);
}
);

View file

@ -1,14 +1,12 @@
import { writable } from "svelte/store";
import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import { VideoPeer } from "../WebRtc/VideoPeer";
import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import { get, writable } from "svelte/store";
import type { Streamable } from "./StreamableCollectionStore";
import { peerStore } from "./PeerStore";
/**
* A store that contains the peer / media that has currently the "importance" focus.
*/
function createVideoFocusStore() {
const { subscribe, set, update } = writable<Streamable | null>(null);
const { subscribe, set } = writable<Streamable | null>(null);
let focusedMedia: Streamable | null = null;
@ -23,27 +21,17 @@ function createVideoFocusStore() {
set(null);
},
toggleFocus: (media: Streamable) => {
if (media !== focusedMedia) {
focusedMedia = media;
} else {
focusedMedia = null;
}
focusedMedia = media !== focusedMedia ? media : null;
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();
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 { localUserStore } from "../Connexion/LocalUserStore";
export enum GameConnexionTypes {
anonymous = 1,
@ -7,13 +8,16 @@ export enum GameConnexionTypes {
empty,
unknown,
jwt,
login,
}
//this class is responsible with analysing and editing the game's url
class UrlManager {
public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString();
if (url === "/jwt") {
if (url === "/login") {
return GameConnexionTypes.login;
} else if (url === "/jwt") {
return GameConnexionTypes.jwt;
} else if (url.includes("_/")) {
return GameConnexionTypes.anonymous;
@ -35,6 +39,8 @@ class UrlManager {
public pushRoomIdToUrl(room: Room): void {
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 search = room.search.toString();
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);

View file

@ -48,6 +48,10 @@ class CoWebsiteManager {
this.cowebsiteDiv.style.width = width + "px";
}
set widthPercent(width: number) {
this.cowebsiteDiv.style.width = width + "%";
}
get height(): number {
return this.cowebsiteDiv.clientHeight;
}
@ -162,7 +166,13 @@ class CoWebsiteManager {
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.cowebsiteMainDom.innerHTML = ``;
@ -186,6 +196,9 @@ class CoWebsiteManager {
.then(() => Promise.race([onloadPromise, onTimeoutPromise]))
.then(() => {
this.open();
if (widthPercent) {
this.widthPercent = widthPercent;
}
setTimeout(() => {
this.fire();
}, animationTime);
@ -199,13 +212,16 @@ class CoWebsiteManager {
/**
* 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.cowebsiteMainDom.innerHTML = ``;
this.currentOperationPromise = this.currentOperationPromise
.then(() => callback(this.cowebsiteMainDom))
.then(() => {
this.open();
if (widthPercent) {
this.widthPercent = widthPercent;
}
setTimeout(() => {
this.fire();
}, animationTime);

View file

@ -1,37 +1,67 @@
import {JITSI_URL} from "../Enum/EnvironmentVariable";
import {mediaManager} from "./MediaManager";
import {coWebsiteManager} from "./CoWebsiteManager";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import {get} from "svelte/store";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { coWebsiteManager } from "./CoWebsiteManager";
import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import { get } from "svelte/store";
interface jitsiConfigInterface {
startWithAudioMuted: boolean
startWithVideoMuted: boolean
prejoinPageEnabled: boolean
startWithAudioMuted: boolean;
startWithVideoMuted: boolean;
prejoinPageEnabled: boolean;
}
const getDefaultConfig = () : jitsiConfigInterface => {
return {
startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false
interface JitsiOptions {
jwt?: string;
roomName: string;
width: string;
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 currentDefaultConfig = getDefaultConfig();
if(!config){
if (!config) {
return currentDefaultConfig;
}
return {
...currentDefaultConfig,
...config,
startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted ? true : currentDefaultConfig.startWithAudioMuted,
startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted ? true : currentDefaultConfig.startWithVideoMuted,
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled ? true : currentDefaultConfig.prejoinPageEnabled
}
}
startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted
? true
: currentDefaultConfig.startWithAudioMuted,
startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted
? true
: currentDefaultConfig.startWithVideoMuted,
prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled
? true
: currentDefaultConfig.prejoinPageEnabled,
};
};
const defaultInterfaceConfig = {
SHOW_CHROME_EXTENSION_BANNER: false,
@ -49,28 +79,48 @@ const defaultInterfaceConfig = {
SHOW_WATERMARK_FOR_GUESTS: false,
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', /*'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'*/
"microphone",
"camera",
"closedcaptions",
"desktop",
/*'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 value = args.join(' ')
const value = args.join(" ");
return value
.normalize('NFD') // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.normalize("NFD") // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator
}
.replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, "-"); // separator
};
class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
private jitsiApi?: JitsiApi;
private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this);
private jitsiScriptLoaded: boolean = false;
@ -79,11 +129,19 @@ class JitsiFactory {
* Slugifies the room name and prepends the room name with the instance
*/
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 {
coWebsiteManager.insertCoWebsite((async cowebsiteDiv => {
public start(
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
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
@ -94,18 +152,18 @@ class JitsiFactory {
const domain = jitsiUrl || JITSI_URL;
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);
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
const options: JitsiOptions = {
roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: cowebsiteDiv,
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig}
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!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.
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.executeCommand('displayName', playerName);
this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
});
}));
}, jitsiWidth);
}
public async stop(): Promise<void> {
if(!this.jitsiApi){
if (!this.jitsiApi) {
return;
}
await coWebsiteManager.closeCoWebsite();
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);
this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback);
this.jitsiApi?.dispose();
}
private onAudioChange({muted}: {muted: boolean}): void {
private onAudioChange({ muted }: { muted: boolean }): void {
if (muted) {
requestedMicrophoneState.disableMicrophone();
} else {
@ -141,7 +199,7 @@ class JitsiFactory {
}
}
private onVideoChange({muted}: {muted: boolean}): void {
private onVideoChange({ muted }: { muted: boolean }): void {
if (muted) {
requestedCameraState.disableWebcam();
} else {
@ -159,20 +217,17 @@ class JitsiFactory {
this.jitsiScriptLoaded = true;
// Load Jitsi if the environment variable is set.
const jitsiScript = document.createElement('script');
jitsiScript.src = 'https://' + domain + '/external_api.js';
const jitsiScript = document.createElement("script");
jitsiScript.src = "https://" + domain + "/external_api.js";
jitsiScript.onload = () => {
resolve();
}
};
jitsiScript.onerror = () => {
reject();
}
};
document.head.appendChild(jitsiScript);
})
});
}
}

View file

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

View file

@ -4,16 +4,13 @@ import type {
} from "../Connexion/ConnexionModels";
import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
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 { blackListManager } from "./BlackListManager";
import { get } from "svelte/store";
import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { discussionManager } from "./DiscussionManager";
import { playersStore } from "../Stores/PlayersStore";
import { newChatMessageStore } from "../Stores/ChatStore";
import { isMobile } from "../Enum/EnvironmentVariable";
import { peerStore, screenSharingPeerStore } from "../Stores/PeerStore";
export interface UserSimplePeerInterface {
userId: number;
@ -24,12 +21,6 @@ export interface UserSimplePeerInterface {
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.
*/
@ -41,24 +32,21 @@ export class SimplePeer {
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number;
private lastWebrtcUserName: 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.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
this.unsubscribers.push(
localStreamStore.subscribe((streamResult) => {
this.sendLocalVideoStream(streamResult);
})
);
let localScreenCapture: MediaStream | null = null;
//todo
this.unsubscribers.push(
screenSharingLocalStreamStore.subscribe((streamResult) => {
if (streamResult.type === "error") {
@ -82,14 +70,6 @@ export class SimplePeer {
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
*/
@ -126,19 +106,14 @@ export class SimplePeer {
if (!user.initiator) {
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
*/
private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null {
private createPeerConnection(user: UserSimplePeerInterface): VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId);
if (peerConnection) {
if (peerConnection.destroyed) {
@ -160,7 +135,7 @@ export class SimplePeer {
this.lastWebrtcUserName = user.webRtcUser;
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;
// 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);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(peer);
}
peerStore.pushNewPeer(peer);
return peer;
}
@ -204,7 +177,7 @@ export class SimplePeer {
if (!peerConnexionDeleted) {
throw "Error to delete peer connection";
}
this.createPeerConnection(user, stream);
this.createPeerConnection(user);
} else {
peerConnection.toClose = false;
}
@ -228,9 +201,7 @@ export class SimplePeer {
);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(peer);
}
screenSharingPeerStore.pushNewPeer(peer);
return peer;
}
@ -269,12 +240,11 @@ export class SimplePeer {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId);
screenSharingPeerStore.removePeer(userId);
}
}
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId);
}
peerStore.removePeer(userId);
}
/**
@ -282,7 +252,6 @@ export class SimplePeer {
*/
private closeScreenSharingConnection(userId: number) {
try {
//mediaManager.removeActiveScreenSharingVideo("" + userId);
const peer = this.PeerScreenSharingConnectionArray.get(userId);
if (peer === undefined) {
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"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
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) {
console.error("closeConnection", err);
}
@ -323,6 +286,8 @@ export class SimplePeer {
for (const unsubscriber of this.unsubscribers) {
unsubscriber();
}
peerStore.cleanupStore();
screenSharingPeerStore.cleanupStore();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -330,13 +295,7 @@ export class SimplePeer {
try {
//if offer type, create peer connection
if (data.signal.type === "offer") {
const streamResult = get(localStreamStore);
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream;
}
this.createPeerConnection(data, stream);
this.createPeerConnection(data);
}
const peer = this.PeerConnectionArray.get(data.userId);
if (peer !== undefined) {
@ -352,7 +311,6 @@ export class SimplePeer {
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore);
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream !== null) {
@ -379,48 +337,10 @@ export class SimplePeer {
} catch (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
//this.PeerScreenSharingConnectionArray.delete(data.userId);
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) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) {
@ -433,12 +353,6 @@ export class SimplePeer {
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
*/
@ -492,8 +406,6 @@ export class SimplePeer {
if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) {
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 type { Subscription } from "rxjs";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { get, readable, Readable, Unsubscriber } from "svelte/store";
import { obtainedMediaConstraintIsMobileStore, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { readable, Readable, Unsubscriber } from "svelte/store";
import {
localStreamStore,
obtainedMediaConstraintIsMobileStore,
obtainedMediaConstraintStore,
ObtainedMediaStreamConstraints,
} from "../Stores/MediaStore";
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 { isMobile } from "../Enum/EnvironmentVariable";
@ -34,16 +39,17 @@ export class VideoPeer extends Peer {
private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
public readonly constraintsStore: Readable<ObtainedMediaStreamConstraints | null>;
private newMessageunsubscriber: Unsubscriber | null = null;
private closing: Boolean = false; //this is used to prevent destroy() from being called twice
private localStreamStoreSubscribe: Unsubscriber;
private obtainedMediaConstraintStoreSubscribe: Unsubscriber;
constructor(
public user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
localStream: MediaStream | null
private connection: RoomConnection
) {
super({
initiator,
@ -60,27 +66,15 @@ export class VideoPeer extends Peer {
const onStream = (stream: MediaStream | null) => {
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("data", onData);
return () => {
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 message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
@ -191,7 +185,6 @@ export class VideoPeer extends Peer {
this._onFinish();
});
this.pushVideoToRemoteUser(localStream);
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => {
if (userUuid === this.userUuid) {
this.toggleRemoteStream(false);
@ -208,6 +201,21 @@ export class VideoPeer extends Peer {
if (blackListManager.isBlackListed(this.userUuid)) {
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) {
@ -264,6 +272,8 @@ export class VideoPeer extends Peer {
this.onUnBlockSubscribe.unsubscribe();
if (this.newMessageunsubscriber) this.newMessageunsubscriber();
chatMessagesStore.addOutcomingUser(this.userId);
if (this.localStreamStoreSubscribe) this.localStreamStoreSubscribe();
if (this.obtainedMediaConstraintStoreSubscribe) this.obtainedMediaConstraintStoreSubscribe();
super.destroy();
} catch (err) {
console.error("VideoPeer::destroy", err);
@ -281,28 +291,4 @@ export class VideoPeer extends Peer {
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 { EntryScene } from "./Phaser/Login/EntryScene";
import { coWebsiteManager } from "./WebRtc/CoWebsiteManager";
import { MenuScene } from "./Phaser/Menu/MenuScene";
import { localUserStore } from "./Connexion/LocalUserStore";
import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene";
import { iframeListener } from "./Api/IframeListener";
@ -97,7 +96,6 @@ const config: GameConfig = {
ReconnectingScene,
ErrorScene,
CustomizeScene,
MenuScene,
],
//resolution: window.devicePixelRatio / 2,
fps: fps,

View file

@ -1,10 +1,10 @@
import type Phaser from "phaser";
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> {
left: CursorKey;
@ -21,4 +21,3 @@ export interface IVirtualJoystick extends Phaser.GameObjects.GameObject {
visible: boolean;
createCursorKeys: () => CursorKeys;
}

View file

@ -1,15 +1,15 @@
//InputTextGlobalMessage
//TextGlobalMessage
section.section-input-send-text {
--height-toolbar: 15%;
height: 100%;
.ql-toolbar{
max-height: 100px;
height: var(--height-toolbar);
background: whitesmoke;
}
div.input-send-text{
height: calc(100% - 100px);
height: calc(100% - var(--height-toolbar));
overflow: auto;
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 "mobile-style.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;
}
* 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
* 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 ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import { PROFILE_URL } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -190,6 +191,8 @@ module.exports = {
PUSHER_URL: undefined,
UPLOADER_URL: null,
ADMIN_URL: undefined,
CONTACT_URL: null,
PROFILE_URL: null,
DEBUG_MODE: null,
STUN_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==
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.7:
version "0.1.7"
@ -5745,9 +5745,9 @@ urix@^0.1.0:
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b"
integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==
version "1.5.3"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
dependencies:
querystringify "^2.1.1"
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