Merge branch 'develop' of ssh://git.bstly.de:222/_Bastler/partey_workadventure

This commit is contained in:
_Bastler
2022-02-19 10:15:56 +01:00
163 changed files with 5958 additions and 2614 deletions
+3
View File
@@ -15,6 +15,9 @@ export class DebugController {
(async () => { (async () => {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (ADMIN_API_TOKEN === "") {
return res.writeStatus("401 Unauthorized").end("No token configured!");
}
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
} }
+1 -1
View File
@@ -2,7 +2,7 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIM
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "";
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ""; const JITSI_ISS = process.env.JITSI_ISS || "";
+102 -4
View File
@@ -1,20 +1,118 @@
# Security
#
SECRET_KEY=
ADMIN_API_TOKEN=
#
# Networking
#
# The base domain # The base domain
DOMAIN=workadventure.localhost DOMAIN=workadventure.localhost
DEBUG_MODE=false # Subdomains
# MUST match the DOMAIN variable above
FRONT_HOST=front.workadventure.localhost
PUSHER_HOST=pusher.workadventure.localhost
BACK_HOST=api.workadventure.localhost
MAPS_HOST=maps.workadventure.localhost
ICON_HOST=icon.workadventure.localhost
# SAAS admin panel
ADMIN_API_URL=
#
# Basic configuration
#
# The directory to store data in
DATA_DIR=./wa
# The URL used by default, in the form: "/_/global/map/url.json"
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json
# If you want to have a contact page in your menu,
# you MUST set CONTACT_URL to the URL of the page that you want
CONTACT_URL=
MAX_PER_GROUP=4
MAX_USERNAME_LENGTH=8
DISABLE_ANONYMOUS=false
# The version of the docker image to use
# MUST uncomment "image" keys in the docker-compose file for it to be effective
VERSION=master
TZ=Europe/Paris
#
# Jitsi
#
JITSI_URL=meet.jit.si JITSI_URL=meet.jit.si
# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret # If your Jitsi environment has authentication set up,
# you MUST set JITSI_PRIVATE_MODE to "true"
# and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret
JITSI_PRIVATE_MODE=false JITSI_PRIVATE_MODE=false
JITSI_ISS= JITSI_ISS=
SECRET_JITSI_KEY= SECRET_JITSI_KEY=
#
# Turn/Stun
#
# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections) # URL of the TURN server (needed to "punch a hole" through some networks for P2P connections)
TURN_SERVER= TURN_SERVER=
TURN_USER= TURN_USER=
TURN_PASSWORD= TURN_PASSWORD=
# If your Turn server is configured to use the Turn REST API, you MUST put the shared auth secret here.
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
# Keep empty if you are sharing hard coded / clear text credentials.
TURN_STATIC_AUTH_SECRET=
# URL of the STUN server
STUN_SERVER=
# The URL used by default, in the form: "/_/global/map/url.json" #
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json # Certificate config
#
# The email address used by Let's encrypt to send renewal warnings (compulsory) # The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL= ACME_EMAIL=
#
# Additional app configs
# Configuration for apps which are not workadventure itself
#
# openID
OPID_CLIENT_ID=
OPID_CLIENT_SECRET=
OPID_CLIENT_ISSUER=
OPID_CLIENT_REDIRECT_URL=
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
OPID_PROFILE_SCREEN_PROVIDER=
#
# Advanced configuration
# Generally does not need to be changed
#
# Networking
HTTP_PORT=80
HTTPS_PORT=443
# Workadventure settings
DISABLE_NOTIFICATIONS=false
SKIP_RENDER_OPTIMIZATIONS=false
STORE_VARIABLES_FOR_LOCAL_MAPS=true
# Debugging options
DEBUG_MODE=false
LOG_LEVEL=WARN
# Internal URLs
API_URL=back:50051
RESTART_POLICY=unless-stopped
+70 -56
View File
@@ -1,114 +1,128 @@
version: "3.3" version: "3.5"
services: services:
reverse-proxy: reverse-proxy:
image: traefik:v2.3 image: traefik:v2.6
command: command:
- --log.level=WARN - --log.level=${LOG_LEVEL}
#- --api.insecure=true
- --providers.docker - --providers.docker
- --entryPoints.web.address=:80 # Entry points
- --entryPoints.web.address=:${HTTP_PORT}
- --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https - --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entryPoints.websecure.address=:443 - --entryPoints.websecure.address=:${HTTPS_PORT}
# HTTP challenge
- --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL} - --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}
- --certificatesresolvers.myresolver.acme.storage=/acme.json - --certificatesresolvers.myresolver.acme.storage=/acme.json
# used during the challenge
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
# Let's Encrypt's staging server
# uncomment during testing to avoid rate limiting
#- --certificatesresolvers.dnsresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
ports: ports:
- "80:80" - "${HTTP_PORT}:80"
- "443:443" - "${HTTPS_PORT}:443"
# The Web UI (enabled by --api.insecure=true)
#- "8080:8080"
depends_on:
- pusher
- front
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json - ${DATA_DIR}/letsencrypt/acme.json:/acme.json
restart: unless-stopped restart: ${RESTART_POLICY}
front: front:
build: build:
context: ../.. context: ../..
dockerfile: front/Dockerfile dockerfile: front/Dockerfile
#image: thecodingmachine/workadventure-front:master #image: thecodingmachine/workadventure-front:${VERSION}
environment: environment:
DEBUG_MODE: "$DEBUG_MODE" - DEBUG_MODE
JITSI_URL: $JITSI_URL - JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - JITSI_PRIVATE_MODE
PUSHER_URL: //pusher.${DOMAIN} - PUSHER_URL=//${PUSHER_HOST}
ICON_URL: //icon.${DOMAIN} - ICON_URL=//${ICON_HOST}
TURN_SERVER: "${TURN_SERVER}" - TURN_SERVER
TURN_USER: "${TURN_USER}" - TURN_USER
TURN_PASSWORD: "${TURN_PASSWORD}" - TURN_PASSWORD
START_ROOM_URL: "${START_ROOM_URL}" - TURN_STATIC_AUTH_SECRET
- STUN_SERVER
- START_ROOM_URL
- SKIP_RENDER_OPTIMIZATIONS
- MAX_PER_GROUP
- MAX_USERNAME_LENGTH
- DISABLE_ANONYMOUS
- DISABLE_NOTIFICATIONS
labels: labels:
- "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)" - "traefik.http.routers.front.rule=Host(`${FRONT_HOST}`)"
- "traefik.http.routers.front.entryPoints=web,traefik" - "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=80" - "traefik.http.services.front.loadbalancer.server.port=80"
- "traefik.http.routers.front-ssl.rule=Host(`play.${DOMAIN}`)" - "traefik.http.routers.front-ssl.rule=Host(`${FRONT_HOST}`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure" - "traefik.http.routers.front-ssl.entryPoints=websecure"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.service=front" - "traefik.http.routers.front-ssl.service=front"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.tls.certresolver=myresolver" - "traefik.http.routers.front-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: ${RESTART_POLICY}
pusher: pusher:
build: build:
context: ../.. context: ../..
dockerfile: pusher/Dockerfile dockerfile: pusher/Dockerfile
#image: thecodingmachine/workadventure-pusher:master #image: thecodingmachine/workadventure-pusher:${VERSION}
command: yarn run runprod command: yarn run runprod
environment: environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_JITSI_KEY
SECRET_KEY: yourSecretKey - SECRET_KEY
API_URL: back:50051 - API_URL
JITSI_URL: $JITSI_URL - FRONT_URL=https://${FRONT_HOST}
JITSI_ISS: $JITSI_ISS - JITSI_URL
FRONT_URL: https://play.${DOMAIN} - JITSI_ISS
- DISABLE_ANONYMOUS
labels: labels:
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)" - "traefik.http.routers.pusher.rule=Host(`${PUSHER_HOST}`)"
- "traefik.http.routers.pusher.entryPoints=web,traefik" - "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080" - "traefik.http.services.pusher.loadbalancer.server.port=8080"
- "traefik.http.routers.pusher-ssl.rule=Host(`pusher.${DOMAIN}`)" - "traefik.http.routers.pusher-ssl.rule=Host(${PUSHER_HOST}`)"
- "traefik.http.routers.pusher-ssl.entryPoints=websecure" - "traefik.http.routers.pusher-ssl.entryPoints=websecure"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher" - "traefik.http.routers.pusher-ssl.service=pusher"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver" - "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: ${RESTART_POLICY}
back: back:
build: build:
context: ../.. context: ../..
dockerfile: back/Dockerfile dockerfile: back/Dockerfile
#image: thecodingmachine/workadventure-back:master #image: thecodingmachine/workadventure-back:${VERSION}
command: yarn run runprod command: yarn run runprod
environment: environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_JITSI_KEY
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - SECRET_KEY
ADMIN_API_URL: "$ADMIN_API_URL" - ADMIN_API_TOKEN
JITSI_URL: $JITSI_URL - ADMIN_API_URL
JITSI_ISS: $JITSI_ISS - TURN_SERVER
- TURN_USER
- TURN_PASSWORD
- TURN_STATIC_AUTH_SECRET
- STUN_SERVER
- JITSI_URL
- JITSI_ISS
- MAX_PER_GROUP
- STORE_VARIABLES_FOR_LOCAL_MAPS
labels: labels:
- "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)" - "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back.entryPoints=web" - "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080" - "traefik.http.services.back.loadbalancer.server.port=8080"
- "traefik.http.routers.back-ssl.rule=Host(`api.${DOMAIN}`)" - "traefik.http.routers.back-ssl.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back-ssl.entryPoints=websecure" - "traefik.http.routers.back-ssl.entryPoints=websecure"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.service=back" - "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver" - "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: ${RESTART_POLICY}
icon: icon:
image: matthiasluedtke/iconserver:v3.13.0 image: matthiasluedtke/iconserver:v3.13.0
labels: labels:
- "traefik.http.routers.icon.rule=Host(`icon.${DOMAIN}`)" - "traefik.http.routers.icon.rule=Host(`${ICON_HOST}`)"
- "traefik.http.routers.icon.entryPoints=web,traefik" - "traefik.http.routers.icon.entryPoints=web,traefik"
- "traefik.http.services.icon.loadbalancer.server.port=8080" - "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=Host(`icon.${DOMAIN}`)" - "traefik.http.routers.icon-ssl.rule=Host(`${ICON_HOST}`)"
- "traefik.http.routers.icon-ssl.entryPoints=websecure" - "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon" - "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver" - "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"
+21 -2
View File
@@ -83,7 +83,8 @@
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json" "START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json",
"ICON_URL": "//icon-"+url,
} }
}, },
"uploader": { "uploader": {
@@ -109,7 +110,15 @@
"redis": { "redis": {
"image": "redis:6", "image": "redis:6",
"ports": [6379] "ports": [6379]
} },
"iconserver": {
"image": "matthiasluedtke/iconserver:v3.13.0",
"host": {
"url": "icon-"+url,
"containerPort": 8080,
},
"ports": [8080]
},
}, },
"config": { "config": {
k8sextension(k8sConf):: k8sextension(k8sConf)::
@@ -210,6 +219,16 @@
} }
} }
}, },
iconserver+: {
ingress+: {
spec+: {
tls+: [{
hosts: ["icon-"+url],
secretName: "certificate-tls"
}]
}
}
},
} }
} }
} }
+14 -15
View File
@@ -3,7 +3,7 @@
### Opening a web page in a new tab ### Opening a web page in a new tab
``` ```ts
WA.nav.openTab(url: string): void WA.nav.openTab(url: string): void
``` ```
@@ -11,13 +11,13 @@ Opens the webpage at "url" in your browser, in a new tab.
Example: Example:
```javascript ```ts
WA.nav.openTab('https://www.wikipedia.org/'); WA.nav.openTab('https://www.wikipedia.org/');
``` ```
### Opening a web page in the current tab ### Opening a web page in the current tab
``` ```ts
WA.nav.goToPage(url: string): void WA.nav.goToPage(url: string): void
``` ```
@@ -25,14 +25,13 @@ Opens the webpage at "url" in your browser in place of WorkAdventure. WorkAdvent
Example: Example:
```javascript ```ts
WA.nav.goToPage('https://www.wikipedia.org/'); WA.nav.goToPage('https://www.wikipedia.org/');
``` ```
### Going to a different map from the script ### Going to a different map from the script
``` ```ts
WA.nav.goToRoom(url: string): void WA.nav.goToRoom(url: string): void
``` ```
@@ -43,7 +42,7 @@ global urls: "/_/global/domain/path/map.json[#start-layer-name]"
Example: Example:
```javascript ```ts
WA.nav.goToRoom("/@/tcm/workadventure/floor0") // workadventure urls WA.nav.goToRoom("/@/tcm/workadventure/floor0") // workadventure urls
WA.nav.goToRoom('../otherMap/map.json'); WA.nav.goToRoom('../otherMap/map.json');
WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2") WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
@@ -51,25 +50,25 @@ WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
### Opening/closing web page in Co-Websites ### Opening/closing web page in Co-Websites
``` ```ts
WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number = 0): Promise<CoWebsite> WA.nav.openCoWebSite(url: string, allowApi?: boolean = false, allowPolicy?: string = "", percentWidth?: number, position?: number, closable?: boolean, lazy?: boolean): Promise<CoWebsite>
``` ```
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open. Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow),widthPercent define the width of the main cowebsite beetween the min size and the max size (70% of the viewport), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy
You can have only 5 co-wbesites open simultaneously. it's to add the cowebsite but don't load it.
Example: Example:
```javascript ```ts
const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/'); const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/');
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1); const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 70, 1, true, true);
// ... // ...
coWebsite.close(); coWebsite.close();
``` ```
### Get all Co-Websites ### Get all Co-Websites
``` ```ts
WA.nav.getCoWebSites(): Promise<CoWebsite[]> WA.nav.getCoWebSites(): Promise<CoWebsite[]>
``` ```
@@ -77,6 +76,6 @@ Get all opened co-websites with their ids and positions.
Example: Example:
```javascript ```ts
const coWebsites = await WA.nav.getCowebSites(); const coWebsites = await WA.nav.getCowebSites();
``` ```
+5 -1
View File
@@ -82,7 +82,11 @@ We are able to direct a Woka to the desired place immediately after spawn. To ma
``` ```
.../my_map.json#moveTo=meeting-room&start .../my_map.json#moveTo=meeting-room&start
``` ```
*...or even like this!*
```
.../my_map.json#start&moveTo=200,100
```
For this to work, moveTo must be equal to the layer name of interest. This layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected. For this to work, moveTo must be equal to the x and y position, layer name, or object name of interest. Layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected.
![](images/moveTo-layer-example.png) ![](images/moveTo-layer-example.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+6
View File
@@ -141,6 +141,12 @@ return [
'markdown' => 'maps.api-controls', 'markdown' => 'maps.api-controls',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-controls.md', 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-controls.md',
], ],
[
'title' => 'Camera',
'url' => '/map-building/api-camera.md',
'markdown' => 'maps.api-camera',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-camera.md',
],
[ [
'title' => 'Deprecated', 'title' => 'Deprecated',
'url' => '/map-building/api-deprecated.md', 'url' => '/map-building/api-deprecated.md',
+7
View File
@@ -52,6 +52,13 @@ If you set `openWebsiteTrigger: onaction`, when the user walks on the layer, an
If you set `openWebsiteTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to open the web site'. If you set `openWebsiteTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to open the web site'.
If you set `openWebsiteTrigger: onicon`, when the user walks on the layer, an icon will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/icon_open_website.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The iFrame will only open if the user clicks on icon</figcaption>
</figure>
### Setting the iFrame "allow" attribute ### Setting the iFrame "allow" attribute
By default, iFrames have limited rights in browsers. For instance, they cannot put their content in fullscreen, they cannot start your webcam, etc... By default, iFrames have limited rights in browsers. For instance, they cannot put their content in fullscreen, they cannot start your webcam, etc...
+1 -2
View File
@@ -1,5 +1,4 @@
{ {
"printWidth": 120, "printWidth": 120,
"tabWidth": 4, "tabWidth": 4
"plugins": ["prettier-plugin-svelte"]
} }
+3 -1
View File
@@ -1,4 +1,6 @@
index.html index.html
index.tmpl.html.tmp
/js/ /js/
/fonts/
style.*.css style.*.css
!env-config.template.js
*.png
+27
View File
@@ -0,0 +1,27 @@
window.env = {
SKIP_RENDER_OPTIMIZATIONS: '${SKIP_RENDER_OPTIMIZATIONS}',
DISABLE_NOTIFICATIONS: '${DISABLE_NOTIFICATIONS}',
PUSHER_URL: '${PUSHER_URL}',
UPLOADER_URL: '${UPLOADER_URL}',
ADMIN_URL: '${ADMIN_URL}',
CONTACT_URL: '${CONTACT_URL}',
PROFILE_URL: '${PROFILE_URL}',
ICON_URL: '${ICON_URL}',
DEBUG_MODE: '${DEBUG_MODE}',
STUN_SERVER: '${STUN_SERVER}',
TURN_SERVER: '${TURN_SERVER}',
TURN_USER: '${TURN_USER}',
TURN_PASSWORD: '${TURN_PASSWORD}',
JITSI_URL: '${JITSI_URL}',
JITSI_PRIVATE_MODE: '${JITSI_PRIVATE_MODE}',
START_ROOM_URL: '${START_ROOM_URL}',
MAX_USERNAME_LENGTH: '${MAX_USERNAME_LENGTH}',
MAX_PER_GROUP: '${MAX_PER_GROUP}',
DISPLAY_TERMS_OF_USE: '${DISPLAY_TERMS_OF_USE}',
POSTHOG_API_KEY: '${POSTHOG_API_KEY}',
POSTHOG_URL: '${POSTHOG_URL}',
NODE_ENV: '${NODE_ENV}',
DISABLE_ANONYMOUS: '${DISABLE_ANONYMOUS}',
OPID_LOGIN_SCREEN_PROVIDER: '${OPID_LOGIN_SCREEN_PROVIDER}',
FALLBACK_LOCALE: '${FALLBACK_LOCALE}',
};
+68
View File
File diff suppressed because one or more lines are too long
-124
View File
@@ -1,124 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- TRACK CODE -->
<!-- END TRACK CODE -->
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicons/favicon-16x16.png">
<link rel="manifest" href="static/images/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<base href="/">
<title>Partey</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">
<div id="cowebsite-container">
<div id="cowebsite-container-main">
<div id="cowebsite-slot-1">
<div class="actions">
<button type="button" class="nes-btn is-primary expand">></button>
<button type="button" class="nes-btn is-error close">&times;</button>
</div>
</div>
</div>
<div id="cowebsite-container-sub">
<div id="cowebsite-slot-2">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
<div id="cowebsite-slot-3">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
<div id="cowebsite-slot-4">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
</div>
</div>
<div id="svelte-overlay"></div>
<div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section">
</div>
<aside id="sidebar" class="sidebar">
</aside>
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
</div>
</div>
</div>
<div id="cowebsite" class="cowebsite hidden">
<aside id="cowebsite-aside" class="noselect">
<div id="cowebsite-aside-buttons">
<button class="top-right-btn nes-btn is-error" id="cowebsite-close" alt="close all co-websites">
&times;
</button>
<button class="top-right-btn nes-btn is-primary" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
<img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
</button>
</div>
<div id="cowebsite-aside-holder">
<img src="/static/images/menu.svg" alt="hold to resize"/>
</div>
<div id="cowebsite-sub-icons"></div>
</aside>
<main id="cowebsite-slot-0"></main>
</div>
<div id="cowebsite-buffer"></div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><defs><image width="12" height="14" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAOAQMAAAAhc2+vAAAAAXNSR0IB2cksfwAAAAZQTFRFAAAA////pdmf3QAAAAJ0Uk5TAP9bkSK1AAAAJUlEQVR4nGNgOMDAoMDw/wMDCDgwMDQwQIAASBgE/j8ACRswAACLjwYPIknTggAAAABJRU5ErkJggg=="/><image width="12" height="12" id="img2" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAgMAAAArG7R0AAAAAXNSR0IB2cksfwAAAAlQTFRFAAAA/////wAAzV63nAAAAAN0Uk5TAP8BHlUJOgAAACJJREFUeJxjYGAQYGBgYGEIDQ0F0xA+BwMDEi80NASqigEAOD8CVqGVgwsAAAAASUVORK5CYII="/></defs><style></style><use href="#img1" x="2" y="1" /><use href="#img2" x="2" y="2" /></svg>

After

Width:  |  Height:  |  Size: 717 B

+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="452.388px" height="452.388px" viewBox="0 0 452.388 452.388" style="enable-background:new 0 0 452.388 452.388;"
xml:space="preserve">
<g>
<g id="Layer_8_38_">
<path d="M441.677,43.643H10.687C4.785,43.643,0,48.427,0,54.329v297.425c0,5.898,4.785,10.676,10.687,10.676h162.069v25.631
c0,0.38,0.074,0.722,0.112,1.089h-23.257c-5.407,0-9.796,4.389-9.796,9.795c0,5.408,4.389,9.801,9.796,9.801h158.506
c5.406,0,9.795-4.389,9.795-9.801c0-5.406-4.389-9.795-9.795-9.795h-23.256c0.032-0.355,0.115-0.709,0.115-1.089V362.43H441.7
c5.898,0,10.688-4.782,10.688-10.676V54.329C452.37,48.427,447.589,43.643,441.677,43.643z M422.089,305.133
c0,5.903-4.784,10.687-10.683,10.687H40.96c-5.898,0-10.684-4.783-10.684-10.687V79.615c0-5.898,4.786-10.684,10.684-10.684
h370.446c5.898,0,10.683,4.785,10.683,10.684V305.133z M303.942,290.648H154.025c0-29.872,17.472-55.661,42.753-67.706
c-15.987-10.501-26.546-28.571-26.546-49.13c0-32.449,26.306-58.755,58.755-58.755c32.448,0,58.753,26.307,58.753,58.755
c0,20.553-10.562,38.629-26.545,49.13C286.475,234.987,303.942,260.781,303.942,290.648z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+5 -2
View File
@@ -15,6 +15,7 @@
"@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0", "@typescript-eslint/parser": "^5.6.0",
"css-loader": "^5.2.4", "css-loader": "^5.2.4",
"css-minimizer-webpack-plugin": "^3.3.1",
"eslint": "^8.4.1", "eslint": "^8.4.1",
"eslint-plugin-svelte3": "^3.2.1", "eslint-plugin-svelte3": "^3.2.1",
"fork-ts-checker-webpack-plugin": "^6.5.0", "fork-ts-checker-webpack-plugin": "^6.5.0",
@@ -46,6 +47,7 @@
"@types/simple-peer": "^9.11.1", "@types/simple-peer": "^9.11.1",
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.32",
"axios": "^0.21.2", "axios": "^0.21.2",
"cancelable-promise": "^4.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0", "deep-copy-ts": "^0.5.0",
"easystarjs": "^0.4.4", "easystarjs": "^0.4.4",
@@ -71,7 +73,7 @@
"zod": "^3.11.6" "zod": "^3.11.6"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve svelte-check-watch typesafe-i18n", "start": "run-p templater serve svelte-check-watch typesafe-i18n-watch",
"templater": "cross-env ./templater.sh", "templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
@@ -84,7 +86,8 @@
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"", "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'", "pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'", "pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'",
"typesafe-i18n": "typesafe-i18n --no-watch" "typesafe-i18n": "typesafe-i18n --no-watch",
"typesafe-i18n-watch": "typesafe-i18n"
}, },
"lint-staged": { "lint-staged": {
"*.svelte": [ "*.svelte": [
+3 -1
View File
@@ -5,14 +5,16 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface()
url: tg.isString, url: tg.isString,
allowApi: tg.isOptional(tg.isBoolean), allowApi: tg.isOptional(tg.isBoolean),
allowPolicy: tg.isOptional(tg.isString), allowPolicy: tg.isOptional(tg.isString),
widthPercent: tg.isOptional(tg.isNumber),
position: tg.isOptional(tg.isNumber), position: tg.isOptional(tg.isNumber),
closable: tg.isOptional(tg.isBoolean),
lazy: tg.isOptional(tg.isBoolean),
}) })
.get(); .get();
export const isCoWebsite = new tg.IsInterface() export const isCoWebsite = new tg.IsInterface()
.withProperties({ .withProperties({
id: tg.isString, id: tg.isString,
position: tg.isNumber,
}) })
.get(); .get();
+15 -4
View File
@@ -1,7 +1,7 @@
import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution"; import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution";
export class CoWebsite { export class CoWebsite {
constructor(private readonly id: string, public readonly position: number) {} constructor(private readonly id: string) {}
close() { close() {
return queryWorkadventure({ return queryWorkadventure({
@@ -41,17 +41,28 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
}); });
} }
async openCoWebSite(url: string, allowApi?: boolean, allowPolicy?: string, position?: number): Promise<CoWebsite> { async openCoWebSite(
url: string,
allowApi?: boolean,
allowPolicy?: string,
widthPercent?: number,
position?: number,
closable?: boolean,
lazy?: boolean
): Promise<CoWebsite> {
const result = await queryWorkadventure({ const result = await queryWorkadventure({
type: "openCoWebsite", type: "openCoWebsite",
data: { data: {
url, url,
allowApi, allowApi,
allowPolicy, allowPolicy,
widthPercent,
position, position,
closable,
lazy,
}, },
}); });
return new CoWebsite(result.id, result.position); return new CoWebsite(result.id);
} }
async getCoWebSites(): Promise<CoWebsite[]> { async getCoWebSites(): Promise<CoWebsite[]> {
@@ -59,7 +70,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
type: "getCoWebsites", type: "getCoWebsites",
data: undefined, data: undefined,
}); });
return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id, cowebsiteEvent.position)); return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id));
} }
/** /**
@@ -0,0 +1,99 @@
<script lang="typescript">
import { actionsMenuStore } from "../../Stores/ActionsMenuStore";
import { onDestroy } from "svelte";
import type { Unsubscriber } from "svelte/store";
import type { ActionsMenuData } from "../../Stores/ActionsMenuStore";
let actionsMenuData: ActionsMenuData | undefined;
let actionsMenuStoreUnsubscriber: Unsubscriber | null;
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
closeActionsMenu();
}
}
function closeActionsMenu() {
actionsMenuStore.clear();
}
actionsMenuStoreUnsubscriber = actionsMenuStore.subscribe((value) => {
actionsMenuData = value;
});
onDestroy(() => {
if (actionsMenuStoreUnsubscriber) {
actionsMenuStoreUnsubscriber();
}
});
</script>
<svelte:window on:keydown={onKeyDown} />
{#if actionsMenuData}
<div class="actions-menu nes-container is-rounded">
<button type="button" class="nes-btn is-error close" on:click={closeActionsMenu}>&times</button>
<h2>{actionsMenuData.playerName}</h2>
<div class="actions">
{#each [...actionsMenuData.actions] as { actionName, callback }}
<button
type="button"
class="nes-btn"
on:click|preventDefault={() => {
callback();
}}
>
{actionName}
</button>
{/each}
</div>
</div>
{/if}
<style lang="scss">
.actions-menu {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
width: 260px !important;
height: max-content !important;
max-height: 40vh;
margin-top: 200px;
pointer-events: auto;
font-family: "Press Start 2P";
background-color: #333333;
color: whitesmoke;
.actions {
max-height: calc(100% - 50px);
width: 100%;
display: block;
overflow-x: hidden;
overflow-y: auto;
button {
width: calc(100% - 10px);
margin-bottom: 10px;
}
}
.actions::-webkit-scrollbar {
display: none;
}
h2 {
text-align: center;
margin-bottom: 20px;
font-family: "Press Start 2P";
}
.nes-btn.is-error.close {
position: absolute;
top: -20px;
right: -20px;
}
}
</style>
+41 -155
View File
@@ -1,166 +1,52 @@
<script lang="typescript"> <script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import { menuIconVisiblilityStore, menuVisiblilityStore } from "../Stores/MenuStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import { errorStore } from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte";
import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import { requestVisitCardsStore } from "../Stores/GameStore";
import type { Game } from "../Phaser/Game/Game"; import type { Game } from "../Phaser/Game/Game";
import { chatVisibilityStore } from "../Stores/ChatStore"; import { chatVisibilityStore } from "../Stores/ChatStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore"; import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import { errorStore } from "../Stores/ErrorStore";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore"; import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte"; import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import { soundPlayingStore } from "../Stores/SoundPlayingStore"; import Chat from "./Chat/Chat.svelte";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import MainLayout from "./MainLayout.svelte";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import ErrorDialog from "./UI/ErrorDialog.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
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";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
export let game: Game; export let game: Game;
</script> </script>
<div> {#if $errorStore.length > 0}
{#if $loginSceneVisibleStore} <div>
<div class="scrollable"> <ErrorDialog />
<LoginScene {game} /> </div>
</div> {:else if $loginSceneVisibleStore}
{/if} <div class="scrollable">
{#if $selectCharacterSceneVisibleStore} <LoginScene {game} />
<div> </div>
<SelectCharacterScene {game} /> {:else if $selectCharacterSceneVisibleStore}
</div> <div>
{/if} <SelectCharacterScene {game} />
{#if $customCharacterSceneVisibleStore} </div>
<div> {:else if $customCharacterSceneVisibleStore}
<CustomCharacterScene {game} /> <div>
</div> <CustomCharacterScene {game} />
{/if} </div>
{#if $selectCompanionSceneVisibleStore} {:else if $selectCompanionSceneVisibleStore}
<div> <div>
<SelectCompanionScene {game} /> <SelectCompanionScene {game} />
</div> </div>
{/if} {:else if $enableCameraSceneVisibilityStore}
{#if $enableCameraSceneVisibilityStore} <div class="scrollable">
<div class="scrollable"> <EnableCameraScene {game} />
<EnableCameraScene {game} /> </div>
</div> {:else}
{/if} <MainLayout />
{#if $banMessageStore.length > 0}
<div>
<BanMessageContainer />
</div>
{:else if $textMessageStore.length > 0}
<div>
<TextMessageContainer />
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
{#if $audioManagerVisibilityStore}
<div>
<AudioManager />
</div>
{/if}
{#if $layoutManagerVisibilityStore}
<div>
<LayoutManager />
</div>
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<div>
<ReportMenu />
</div>
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<div>
<FollowMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon />
</div>
{/if}
{#if $menuVisiblilityStore}
<div>
<Menu />
</div>
{/if}
{#if $emoteMenuStore}
<div>
<EmoteMenu />
</div>
{/if}
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay />
<MyCamera />
<CameraControls />
</div>
{/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup />
</div>
{/if}
{#if $showLimitRoomModalStore}
<div>
<LimitRoomModal />
</div>
{/if}
{#if $showShareLinkMapModalStore}
<div>
<ShareLinkMapModal />
</div>
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $errorStore.length > 0}
<div>
<ErrorDialog />
</div>
{/if}
{#if $chatVisibilityStore} {#if $chatVisibilityStore}
<Chat /> <Chat />
{/if} {/if}
{#if $warningContainerStore} {/if}
<WarningContainer />
{/if}
</div>
@@ -157,13 +157,16 @@
<style lang="scss"> <style lang="scss">
div.main-audio-manager.nes-container.is-rounded { div.main-audio-manager.nes-container.is-rounded {
position: relative; position: absolute;
top: 0.5rem; top: 1%;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw); width: clamp(200px, 15vw, 15vw);
padding: 3px 3px; padding: 3px 3px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
left: 0;
right: 0;
z-index: 550;
background-color: rgb(0, 0, 0, 0.5); background-color: rgb(0, 0, 0, 0.5);
display: grid; display: grid;
+176 -40
View File
@@ -9,10 +9,15 @@
import microphoneCloseImg from "./images/microphone-close.svg"; import microphoneCloseImg from "./images/microphone-close.svg";
import layoutPresentationImg from "./images/layout-presentation.svg"; import layoutPresentationImg from "./images/layout-presentation.svg";
import layoutChatImg from "./images/layout-chat.svg"; import layoutChatImg from "./images/layout-chat.svg";
import { layoutModeStore } from "../Stores/StreamableCollectionStore"; import followImg from "./images/follow.svg";
import { LayoutMode } from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import { peerStore } from "../Stores/PeerStore"; import { peerStore } from "../Stores/PeerStore";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { embedScreenLayout } from "../Stores/EmbedScreensStore";
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
import { gameManager } from "../Phaser/Game/GameManager";
const gameScene = gameManager.getCurrentGameScene();
function screenSharingClick(): void { function screenSharingClick(): void {
if (isSilent) return; if (isSilent) return;
@@ -42,10 +47,26 @@
} }
function switchLayoutMode() { function switchLayoutMode() {
if ($layoutModeStore === LayoutMode.Presentation) { if ($embedScreenLayout === LayoutMode.Presentation) {
$layoutModeStore = LayoutMode.VideoChat; $embedScreenLayout = LayoutMode.VideoChat;
} else { } else {
$layoutModeStore = LayoutMode.Presentation; $embedScreenLayout = LayoutMode.Presentation;
}
}
function followClick() {
switch ($followStateStore) {
case "off":
gameScene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
break;
case "requesting":
case "active":
case "ending":
gameScene.connection?.emitFollowAbort();
followUsersStore.stopFollowing();
break;
} }
} }
@@ -56,41 +77,156 @@
onDestroy(unsubscribeIsSilent); onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div class="btn-cam-action">
<div class="btn-cam-action"> <div class="btn-layout nes-btn is-dark" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
<div class="btn-layout nes-btn is-dark" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}> {#if $embedScreenLayout === LayoutMode.Presentation}
{#if $layoutModeStore === LayoutMode.Presentation} <img class="noselect" src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" />
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" /> {:else}
{:else} <img class="noselect" src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" />
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" /> {/if}
{/if} </div>
</div>
<div <div
class="btn-monitor nes-btn is-dark" class="btn-follow nes-btn is-dark"
on:click={screenSharingClick} class:hide={($peerStore.size === 0 && $followStateStore === "off") || isSilent}
class:hide={!$screenSharingAvailableStore || isSilent} class:disabled={$followStateStore !== "off"}
class:enabled={$requestedScreenSharingState} on:click={followClick}
> >
{#if $requestedScreenSharingState && !isSilent} <img class="noselect" src={followImg} alt="" />
<img src={monitorImg} alt="Start screen sharing" /> </div>
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing" /> <div
{/if} class="btn-monitor nes-btn is-dark"
</div> on:click={screenSharingClick}
<div class="btn-video nes-btn is-dark" on:click={cameraClick} class:disabled={!$requestedCameraState || class:hide={!$screenSharingAvailableStore || isSilent}
isSilent}> class:enabled={$requestedScreenSharingState}
{#if $requestedCameraState && !isSilent} >
<img src={cinemaImg} alt="Turn on webcam" /> {#if $requestedScreenSharingState && !isSilent}
{:else} <img class="noselect" src={monitorImg} alt="Start screen sharing" />
<img src={cinemaCloseImg} alt="Turn off webcam" /> {:else}
{/if} <img class="noselect" src={monitorCloseImg} alt="Stop screen sharing" />
</div> {/if}
<div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}> </div>
{#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone" /> <div class="btn-video nes-btn is-dark" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{:else} {#if $requestedCameraState && !isSilent}
<img src={microphoneCloseImg} alt="Turn off microphone" /> <img class="noselect" src={cinemaImg} alt="Turn on webcam" />
{/if} {:else}
</div> <img class="noselect" src={cinemaCloseImg} alt="Turn off webcam" />
{/if}
</div>
<div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img class="noselect" src={microphoneImg} alt="Turn on microphone" />
{:else}
<img class="noselect" src={microphoneCloseImg} alt="Turn off microphone" />
{/if}
</div> </div>
</div> </div>
<style lang="scss">
@import "../../style/breakpoints.scss";
.btn-cam-action {
pointer-events: all;
position: absolute;
display: inline-flex;
bottom: 10px;
right: 15px;
width: 360px;
height: 40px;
text-align: center;
align-content: center;
justify-content: flex-end;
z-index: 251;
&:hover {
div.hide {
transform: translateY(60px);
}
}
}
/*btn animation*/
.btn-cam-action div {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
transform: translateY(15px);
transition-timing-function: ease-in-out;
transition: all 0.3s;
margin: 0 4%;
&.hide {
transform: translateY(60px);
}
}
.btn-cam-action div.disabled {
background: #d75555;
}
.btn-cam-action div.enabled {
background: #73c973;
}
.btn-cam-action:hover div {
transform: translateY(0);
}
.btn-cam-action div:hover {
background: #407cf7;
box-shadow: 4px 4px 48px #666;
transition: 120ms;
}
.btn-micro {
pointer-events: auto;
}
.btn-video {
pointer-events: auto;
transition: all 0.25s;
}
.btn-monitor {
pointer-events: auto;
}
.btn-layout {
pointer-events: auto;
transition: all 0.15s;
}
.btn-cam-action div img {
height: 22px;
width: 30px;
position: relative;
}
.btn-follow {
pointer-events: auto;
img {
filter: brightness(0) invert(1);
}
}
@media (hover: none) {
/**
* If we cannot hover over elements, let's display camera button in full.
*/
.btn-cam-action {
div {
transform: translateY(0px);
}
}
}
@include media-breakpoint-up(sm) {
.btn-cam-action {
right: 0;
width: 100%;
height: 40%;
max-height: 40px;
div {
width: 20%;
max-height: 44px;
}
}
}
</style>
+3 -4
View File
@@ -42,9 +42,8 @@
<svelte:window on:keydown={onKeyDown} on:click={onClick} /> <svelte:window on:keydown={onKeyDown} on:click={onClick} />
<aside class="chatWindow nes-container is-rounded is-dark" transition:fly={{ x: -1000, duration: 500 }} <aside class="chatWindow nes-container is-rounded is-dark" transition:fly={{ x: -1000, duration: 500 }} bind:this={chatWindowElement}>
bind:this={chatWindowElement}> <p class="close-icon noselect" on:click={closeChat}>&times</p>
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}> <section class="messagesList" bind:this={listDom}>
<ul> <ul>
<li><p class="system-text">{$LL.chat.intro()}</p></li> <li><p class="system-text">{$LL.chat.intro()}</p></li>
@@ -78,7 +77,7 @@
} }
aside.chatWindow { aside.chatWindow {
z-index: 100; z-index: 1000;
pointer-events: auto; pointer-events: auto;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -83,6 +83,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
form.customCharacterScene { form.customCharacterScene {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; pointer-events: auto;
@@ -129,7 +131,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
form.customCharacterScene button.customCharacterSceneButtonLeft { form.customCharacterScene button.customCharacterSceneButtonLeft {
left: 5vw; left: 5vw;
} }
@@ -0,0 +1,32 @@
<script lang="typescript">
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import MediaBox from "../Video/MediaBox.svelte";
export let highlightedEmbedScreen: EmbedScreen | null;
export let full = false;
$: clickable = !full;
</script>
<aside class="cameras-container" class:full>
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if !highlightedEmbedScreen || highlightedEmbedScreen.type !== "streamable" || (highlightedEmbedScreen.type === "streamable" && highlightedEmbedScreen.embed !== peer)}
<MediaBox streamable={peer} isClickable={clickable} />
{/if}
{/each}
</aside>
<style lang="scss">
.cameras-container {
flex: 0 0 25%;
overflow-y: auto;
overflow-x: hidden;
&:first-child {
margin-top: 2%;
}
&.full {
flex: 0 0 100%;
}
}
</style>
@@ -0,0 +1,324 @@
<script lang="typescript">
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { iframeStates } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
export let index: number;
export let coWebsite: CoWebsite;
export let vertical: boolean;
let icon: HTMLImageElement;
let iconLoaded = false;
let state = coWebsite.getStateSubscriber();
let isJitsi: boolean = coWebsite instanceof JitsiCoWebsite;
const mainState = coWebsiteManager.getMainStateSubscriber();
onMount(() => {
icon.src = isJitsi
? "/resources/logos/meet.svg"
: `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = coWebsite.getUrl().hostname;
icon.onload = () => {
iconLoaded = true;
};
});
async function onClick() {
if (vertical) {
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
if ($mainCoWebsite.getId() === coWebsite.getId()) {
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.displayMain();
} else if ($highlightedEmbedScreen?.type === "cowebsite") {
coWebsiteManager.goToMain($highlightedEmbedScreen.embed);
} else {
coWebsiteManager.hideMain();
}
} else {
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.goToMain(coWebsite);
coWebsiteManager.displayMain();
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
}
}
}
if ($state === "asleep") {
await coWebsiteManager.loadCoWebsite(coWebsite);
}
coWebsiteManager.resizeAllIframes();
}
function noDrag() {
return false;
}
let isHighlight: boolean = false;
let isMain: boolean = false;
$: {
isMain =
$mainState === iframeStates.opened &&
$mainCoWebsite !== undefined &&
$mainCoWebsite.getId() === coWebsite.getId();
isHighlight =
$highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" &&
$highlightedEmbedScreen.embed.getId() === coWebsite.getId();
}
</script>
<div
id={"cowebsite-thumbnail-" + index}
class="cowebsite-thumbnail nes-pointer"
class:asleep={$state === "asleep"}
class:loading={$state === "loading"}
class:ready={$state === "ready"}
class:displayed={isMain || isHighlight}
class:vertical
on:click={onClick}
>
<img
class="cowebsite-icon noselect nes-pointer"
class:hide={!iconLoaded}
class:jitsi={isJitsi}
bind:this={icon}
on:dragstart|preventDefault={noDrag}
alt=""
/>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="cowebsite-icon"
class:hide={iconLoaded}
style="margin: auto; background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%; shape-rendering: auto;"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<rect x="19" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0s"
calcMode="discrete"
/>
</rect><rect x="40" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.125s"
calcMode="discrete"
/>
</rect><rect x="61" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.25s"
calcMode="discrete"
/>
</rect><rect x="19" y="40" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.875s"
calcMode="discrete"
/>
</rect><rect x="61" y="40" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.375s"
calcMode="discrete"
/>
</rect><rect x="19" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.75s"
calcMode="discrete"
/>
</rect><rect x="40" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.625s"
calcMode="discrete"
/>
</rect><rect x="61" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.5s"
calcMode="discrete"
/>
</rect>
</svg>
</div>
<style lang="scss">
.cowebsite-thumbnail {
position: relative;
padding: 0;
background-color: rgba(#000000, 0.6);
margin: 12px;
margin-top: auto;
margin-bottom: auto;
&::before {
content: "";
position: absolute;
width: 58px;
height: 58px;
left: -8px;
top: -8px;
margin: 4px;
border-style: solid;
border-width: 4px;
border-image-slice: 3;
border-image-width: 3;
border-image-repeat: stretch;
border-image-source: url('data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" ?><svg version="1.1" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M3 1 h1 v1 h-1 z M4 1 h1 v1 h-1 z M2 2 h1 v1 h-1 z M5 2 h1 v1 h-1 z M1 3 h1 v1 h-1 z M6 3 h1 v1 h-1 z M1 4 h1 v1 h-1 z M6 4 h1 v1 h-1 z M2 5 h1 v1 h-1 z M5 5 h1 v1 h-1 z M3 6 h1 v1 h-1 z M4 6 h1 v1 h-1 z" fill="rgb(33,37,41)" /></svg>');
border-image-outset: 1;
}
&:not(.vertical) {
transition: all 300ms;
transform: translateY(0px);
}
&.vertical {
margin: 7px;
&::before {
width: 48px;
height: 48px;
}
.cowebsite-icon {
width: 40px;
height: 40px;
}
animation: shake 0.35s ease-in-out;
}
&.displayed {
&:not(.vertical) {
transform: translateY(-15px);
}
}
&.asleep {
filter: grayscale(100%);
--webkit-filter: grayscale(100%);
}
&.loading {
animation: 2500ms ease-in-out 0s infinite alternate backgroundLoading;
}
&.ready {
&::before {
border-image-source: url('data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" ?><svg version="1.1" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M3 1 h1 v1 h-1 z M4 1 h1 v1 h-1 z M2 2 h1 v1 h-1 z M5 2 h1 v1 h-1 z M1 3 h1 v1 h-1 z M6 3 h1 v1 h-1 z M1 4 h1 v1 h-1 z M6 4 h1 v1 h-1 z M2 5 h1 v1 h-1 z M5 5 h1 v1 h-1 z M3 6 h1 v1 h-1 z M4 6 h1 v1 h-1 z" fill="rgb(38, 74, 110)" /></svg>');
}
}
@keyframes backgroundLoading {
0% {
background-color: rgba(#000000, 0.6);
}
100% {
background-color: #25598e;
}
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-15px);
}
}
@keyframes shake {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-10px);
}
40% {
transform: translateX(10px);
}
60% {
transform: translateX(-10px);
}
80% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}
.cowebsite-icon {
width: 50px;
height: 50px;
object-fit: cover;
&.hide {
display: none;
}
&.jitsi {
filter: invert(100%);
-webkit-filter: invert(100%);
padding: 7px;
}
}
}
</style>
@@ -0,0 +1,42 @@
<script lang="typescript">
import { coWebsites } from "../../Stores/CoWebsiteStore";
import CoWebsiteThumbnail from "./CoWebsiteThumbnailSlot.svelte";
export let vertical = false;
</script>
{#if $coWebsites.length > 0}
<div id="cowebsite-thumbnail-container" class:vertical>
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.getId())}
<CoWebsiteThumbnail {index} {coWebsite} {vertical} />
{/each}
</div>
{/if}
<style lang="scss">
#cowebsite-thumbnail-container {
pointer-events: all;
height: 100px;
width: 100%;
display: flex;
position: absolute;
bottom: 5px;
left: 2%;
overflow-x: auto;
overflow-y: hidden;
&.vertical {
height: auto !important;
width: auto !important;
bottom: auto !important;
left: auto !important;
position: relative;
overflow-x: hidden;
overflow-y: auto;
flex-direction: column;
align-items: center;
padding-top: 4px;
padding-bottom: 4px;
}
}
</style>
@@ -0,0 +1,22 @@
<script lang="typescript">
import PresentationLayout from "./Layouts/PresentationLayout.svelte";
import MozaicLayout from "./Layouts/MozaicLayout.svelte";
import { LayoutMode } from "../../WebRtc/LayoutManager";
import { embedScreenLayout } from "../../Stores/EmbedScreensStore";
</script>
<div id="embedScreensContainer">
{#if $embedScreenLayout === LayoutMode.Presentation}
<PresentationLayout />
{:else}
<MozaicLayout />
{/if}
</div>
<style lang="scss">
#embedScreensContainer {
display: flex;
padding-top: 2%;
height: 100%;
}
</style>
@@ -0,0 +1,62 @@
<script lang="ts">
import { onMount } from "svelte";
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import { streamableCollectionStore } from "../../../Stores/StreamableCollectionStore";
import MediaBox from "../../Video/MediaBox.svelte";
let layoutDom: HTMLDivElement;
const resizeObserver = new ResizeObserver(() => {});
onMount(() => {
resizeObserver.observe(layoutDom);
highlightedEmbedScreen.removeHighlight();
});
</script>
<div id="mozaic-layout" bind:this={layoutDom}>
<div
class="media-container"
class:full-width={$streamableCollectionStore.size === 1 || $streamableCollectionStore.size === 2}
class:quarter={$streamableCollectionStore.size === 3 || $streamableCollectionStore.size === 4}
>
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
<MediaBox
streamable={peer}
mozaicSolo={$streamableCollectionStore.size === 1}
mozaicFullWidth={$streamableCollectionStore.size === 1 || $streamableCollectionStore.size === 2}
mozaicQuarter={$streamableCollectionStore.size === 3 || $streamableCollectionStore.size >= 4}
/>
{/each}
</div>
</div>
<style lang="scss">
#mozaic-layout {
height: 100%;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
.media-container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 33.3% 33.3% 33.3%;
align-items: center;
justify-content: center;
overflow-y: auto;
overflow-x: hidden;
&.full-width {
grid-template-columns: 100%;
grid-template-rows: 50% 50%;
}
&.quarter {
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
}
}
}
</style>
@@ -0,0 +1,141 @@
<script lang="ts">
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import CamerasContainer from "../CamerasContainer.svelte";
import MediaBox from "../../Video/MediaBox.svelte";
import { coWebsiteManager } from "../../../WebRtc/CoWebsiteManager";
import { afterUpdate, onMount } from "svelte";
import { isMediaBreakpointDown, isMediaBreakpointUp } from "../../../Utils/BreakpointsUtils";
import { peerStore } from "../../../Stores/PeerStore";
function closeCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
if ($highlightedEmbedScreen.embed.isClosable()) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed);
} else {
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch((err) => {
console.error("Cannot unload co-website", err);
});
}
}
}
afterUpdate(() => {
if ($highlightedEmbedScreen) {
coWebsiteManager.resizeAllIframes();
}
});
let layoutDom: HTMLDivElement;
let displayCoWebsiteContainer = isMediaBreakpointDown("lg");
let displayFullMedias = isMediaBreakpointUp("sm");
const resizeObserver = new ResizeObserver(() => {
displayCoWebsiteContainer = isMediaBreakpointDown("lg");
displayFullMedias = isMediaBreakpointUp("sm");
if (!displayCoWebsiteContainer && $highlightedEmbedScreen && $highlightedEmbedScreen.type === "cowebsite") {
highlightedEmbedScreen.removeHighlight();
}
if (displayFullMedias) {
highlightedEmbedScreen.removeHighlight();
}
});
onMount(() => {
resizeObserver.observe(layoutDom);
});
</script>
<div id="presentation-layout" bind:this={layoutDom} class:full-medias={displayFullMedias}>
{#if displayFullMedias}
<div id="full-medias">
<CamerasContainer full={true} highlightedEmbedScreen={$highlightedEmbedScreen} />
</div>
{:else}
<div id="embed-left-block" class:full={$peerStore.size === 0}>
<div id="main-embed-screen">
{#if $highlightedEmbedScreen}
{#if $highlightedEmbedScreen.type === "streamable"}
{#key $highlightedEmbedScreen.embed.uniqueId}
<MediaBox
isHightlighted={true}
isClickable={true}
streamable={$highlightedEmbedScreen.embed}
/>
{/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.getId()}
<div
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
class="highlighted-cowebsite nes-container is-rounded"
>
<div class="actions">
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
>&times;</button
>
</div>
</div>
{/key}
{/if}
{/if}
</div>
</div>
{#if $peerStore.size > 0}
<CamerasContainer highlightedEmbedScreen={$highlightedEmbedScreen} />
{/if}
{/if}
</div>
<style lang="scss">
#presentation-layout {
height: 100%;
width: 100%;
display: flex;
&.full-medias {
overflow-y: auto;
overflow-x: hidden;
}
}
#embed-left-block {
display: flex;
flex-direction: column;
flex: 0 0 75%;
height: 100%;
width: 75%;
&.full {
flex: 0 0 98% !important;
width: 98% !important;
}
}
#main-embed-screen {
height: 100%;
margin-bottom: 3%;
.highlighted-cowebsite {
height: 100% !important;
width: 96%;
background-color: rgba(#000000, 0.6);
margin: 0 !important;
.actions {
z-index: 200;
position: relative;
display: flex;
flex-direction: row;
justify-content: end;
gap: 2%;
button {
pointer-events: all;
}
}
}
}
</style>
@@ -3,8 +3,8 @@
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { EmojiButton } from "@joeattardi/emoji-button"; import { EmojiButton } from "@joeattardi/emoji-button";
import { isMobile } from "../../Enum/EnvironmentVariable";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
let emojiContainer: HTMLElement; let emojiContainer: HTMLElement;
let picker: EmojiButton; let picker: EmojiButton;
@@ -20,7 +20,7 @@
"--secondary-text-color": "whitesmoke", "--secondary-text-color": "whitesmoke",
"--category-button-color": "whitesmoke", "--category-button-color": "whitesmoke",
}, },
emojisPerRow: isMobile() ? 6 : 8, emojisPerRow: isMediaBreakpointUp("md") ? 6 : 8,
autoFocusSearch: false, autoFocusSearch: false,
style: "native", style: "native",
showPreview: false, showPreview: false,
@@ -86,6 +86,8 @@
height: 100%; height: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: absolute;
z-index: 300;
} }
.emote-menu { .emote-menu {
@@ -127,6 +127,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.enableCameraScene { .enableCameraScene {
pointer-events: auto; pointer-events: auto;
margin: 20px auto 0; margin: 20px auto 0;
@@ -214,7 +216,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
.enableCameraScene h2 { .enableCameraScene h2 {
font-size: 80%; font-size: 80%;
} }
@@ -0,0 +1,32 @@
<script lang="typescript">
import followImg from "../images/follow.svg";
export let hidden: Boolean;
let cancelButton = false;
</script>
<div class="btn-follow" class:hide={hidden} class:cancel={cancelButton}>
<img src={followImg} alt="" />
</div>
<style lang="scss">
.btn-follow {
display: flex;
align-items: center;
justify-content: center;
border: solid 0px black;
width: 44px;
height: 44px;
background: #666;
box-shadow: 2px 2px 24px #444;
border-radius: 48px;
transform: translateY(15px);
transition-timing-function: ease-in-out;
margin: 0 4%;
img {
filter: brightness(0) invert(1);
}
}
</style>
@@ -1,9 +1,5 @@
<!--
vim: ft=typescript
-->
<script lang="ts"> <script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore"; import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@@ -11,11 +7,7 @@ vim: ft=typescript
function name(userId: number): string { function name(userId: number): string {
const user = gameScene.MapPlayersByKey.get(userId); const user = gameScene.MapPlayersByKey.get(userId);
return user ? user.PlayerValue : ""; return user ? user.playerName : "";
}
function sendFollowRequest() {
gameScene.CurrentPlayer.sendFollowRequest();
} }
function acceptFollowRequest() { function acceptFollowRequest() {
@@ -83,7 +75,7 @@ vim: ft=typescript
{#if $followStateStore === "active" || $followStateStore === "ending"} {#if $followStateStore === "active" || $followStateStore === "ending"}
<div class="interact-status nes-container is-rounded"> <div class="interact-status nes-container is-rounded">
<section class="interact-status"> <section>
{#if $followRoleStore === "follower"} {#if $followRoleStore === "follower"}
<p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p> <p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 0} {:else if $followUsersStore.length === 0}
@@ -109,48 +101,27 @@ vim: ft=typescript
</div> </div>
{/if} {/if}
{#if $followStateStore === "off"}
<button
type="button"
class="nes-btn is-primary follow-menu-button"
on:click|preventDefault={sendFollowRequest}
title="Ask others to follow"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
{#if $followRoleStore === "follower"}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop following"><img class="background-img" src={followImg} alt="" /></button
>
{:else}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop leading the way"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{/if}
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container { .nes-container {
padding: 5px; padding: 5px;
} }
div.interact-status { .interact-status {
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
position: relative; position: absolute;
height: 2.7em; max-height: 2.7em;
width: 40vw; width: 40vw;
top: 87vh; top: 87vh;
margin: auto;
text-align: center; text-align: center;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 400;
} }
div.interact-menu { div.interact-menu {
@@ -158,10 +129,14 @@ vim: ft=typescript
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
position: relative; position: absolute;
width: 60vw; width: 60vw;
top: 60vh; top: 60vh;
margin: auto; left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 150;
section.interact-menu-title { section.interact-menu-title {
margin-bottom: 20px; margin-bottom: 20px;
@@ -189,23 +164,16 @@ vim: ft=typescript
} }
} }
.follow-menu-button { @include media-breakpoint-up(md) {
position: absolute; .interact-status {
bottom: 10px; width: 90vw;
left: 10px;
pointer-events: all;
}
@media only screen and (max-width: 800px) {
div.interact-status {
width: 100vw;
top: 78vh; top: 78vh;
font-size: 0.75em; font-size: 0.75em;
} }
div.interact-menu { div.interact-menu {
height: 21vh; max-height: 21vh;
width: 100vw; width: 90vw;
font-size: 0.75em; font-size: 0.75em;
} }
} }
@@ -22,7 +22,7 @@
<form <form
class="helpCameraSettings nes-container" class="helpCameraSettings nes-container"
on:submit|preventDefault={close} on:submit|preventDefault={close}
transition:fly={{ y: -900, duration: 500 }} transition:fly={{ y: -50, duration: 500 }}
> >
<section> <section>
<h2>{$LL.camera.help.title()}</h2> <h2>{$LL.camera.help.title()}</h2>
@@ -55,9 +55,12 @@
background: #eceeee; background: #eceeee;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: 10vh; left: 0;
right: 0;
margin-top: 4%;
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
z-index: 600;
overflow: auto; overflow: auto;
text-align: center; text-align: center;
@@ -35,9 +35,11 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 40px; bottom: 40px;
margin: 0 auto; margin-right: auto;
margin-left: auto;
padding: 0; padding: 0;
width: clamp(200px, 20vw, 20vw); width: clamp(200px, 20vw, 20vw);
z-index: 155;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -45,6 +47,10 @@
animation: moveMessage 0.5s; animation: moveMessage 0.5s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
div {
margin-bottom: 5%;
}
} }
div.nes-container { div.nes-container {
+1 -1
View File
@@ -53,7 +53,7 @@
<section class="terms-and-conditions"> <section class="terms-and-conditions">
<a style="display: none;" href="traduction">Need for traduction</a> <a style="display: none;" href="traduction">Need for traduction</a>
<p> <p>
{$LL.login.terms()} {@html $LL.login.terms()}
</p> </p>
</section> </section>
{/if} {/if}
+176
View File
@@ -0,0 +1,176 @@
<script lang="typescript">
import { onMount } from "svelte";
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import { embedScreenLayout, hasEmbedScreen } from "../Stores/EmbedScreensStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility";
import { requestVisitCardsStore } from "../Stores/GameStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import { layoutManagerActionVisibilityStore } from "../Stores/LayoutManagerStore";
import { menuIconVisiblilityStore, menuVisiblilityStore, warningContainerStore } from "../Stores/MenuStore";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import AudioManager from "./AudioManager/AudioManager.svelte";
import CameraControls from "./CameraControls.svelte";
import EmbedScreensContainer from "./EmbedScreens/EmbedScreensContainer.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import LayoutActionManager from "./LayoutActionManager/LayoutActionManager.svelte";
import Menu from "./Menu/Menu.svelte";
import MenuIcon from "./Menu/MenuIcon.svelte";
import MyCamera from "./MyCamera.svelte";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { isMediaBreakpointDown, isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
import CoWebsitesContainer from "./EmbedScreens/CoWebsitesContainer.svelte";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { actionsMenuStore } from "../Stores/ActionsMenuStore";
import ActionsMenu from "./ActionsMenu/ActionsMenu.svelte";
let mainLayout: HTMLDivElement;
let displayCoWebsiteContainerMd = isMediaBreakpointUp("md");
let displayCoWebsiteContainerLg = isMediaBreakpointDown("lg");
const resizeObserver = new ResizeObserver(() => {
displayCoWebsiteContainerMd = isMediaBreakpointUp("md");
displayCoWebsiteContainerLg = isMediaBreakpointDown("lg");
});
onMount(() => {
resizeObserver.observe(mainLayout);
});
</script>
<div id="main-layout" bind:this={mainLayout}>
<aside id="main-layout-left-aside">
{#if $menuIconVisiblilityStore}
<MenuIcon />
{/if}
{#if $embedScreenLayout === LayoutMode.VideoChat || displayCoWebsiteContainerMd}
<CoWebsitesContainer vertical={true} />
{/if}
</aside>
<section id="main-layout-main">
{#if $menuVisiblilityStore}
<Menu />
{/if}
{#if $banMessageStore.length > 0}
<BanMessageContainer />
{:else if $textMessageStore.length > 0}
<TextMessageContainer />
{/if}
{#if $soundPlayingStore}
<AudioPlaying url={$soundPlayingStore} />
{/if}
{#if $warningContainerStore}
<WarningContainer />
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<ReportMenu />
{/if}
{#if $helpCameraSettingsVisibleStore}
<HelpCameraSettingsPopup />
{/if}
{#if $audioManagerVisibilityStore}
<AudioManager />
{/if}
{#if $showLimitRoomModalStore}
<LimitRoomModal />
{/if}
{#if $showShareLinkMapModalStore}
<ShareLinkMapModal />
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<FollowMenu />
{/if}
{#if $actionsMenuStore}
<ActionsMenu />
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $emoteMenuStore}
<EmoteMenu />
{/if}
{#if hasEmbedScreen}
<EmbedScreensContainer />
{/if}
</section>
<section id="main-layout-baseline">
{#if displayCoWebsiteContainerLg}
<CoWebsitesContainer />
{/if}
{#if $layoutManagerActionVisibilityStore}
<LayoutActionManager />
{/if}
{#if $myCameraVisibilityStore}
<MyCamera />
<CameraControls />
{/if}
</section>
</div>
<style lang="scss">
@import "../../style/breakpoints.scss";
#main-layout {
display: grid;
grid-template-columns: 120px calc(100% - 120px);
grid-template-rows: 80% 20%;
&-left-aside {
min-width: 80px;
}
&-baseline {
grid-column: 1/3;
}
}
@include media-breakpoint-up(md) {
#main-layout {
grid-template-columns: 15% 85%;
&-left-aside {
min-width: auto;
}
}
}
@include media-breakpoint-up(sm) {
#main-layout {
grid-template-columns: 20% 80%;
}
}
</style>
@@ -100,6 +100,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.string-HTML { .string-HTML {
white-space: pre-line; white-space: pre-line;
} }
@@ -126,7 +128,7 @@
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @include media-breakpoint-up(md) {
div.about-room-main { div.about-room-main {
section.container-overflow { section.container-overflow {
height: calc(100% - 120px); height: calc(100% - 120px);
@@ -67,6 +67,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.global-message-main { div.global-message-main {
height: calc(100% - 50px); height: calc(100% - 50px);
display: grid; display: grid;
@@ -109,7 +111,7 @@
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @include media-breakpoint-up(md) {
.global-message-content { .global-message-content {
height: calc(100% - 5px); height: calc(100% - 5px);
} }
+85 -23
View File
@@ -1,5 +1,12 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
let entryPoint: string = $startLayerNamesStore[0];
let walkAutomatically: boolean = false;
const currentPlayer = gameManager.getCurrentGameScene().CurrentPlayer;
const playerPos = { x: Math.floor(currentPlayer.x), y: Math.floor(currentPlayer.y) };
function copyLink() { function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement; const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@@ -8,8 +15,23 @@
document.execCommand("copy"); document.execCommand("copy");
} }
function getLink() {
return `${location.origin}${location.pathname}#${entryPoint}${
walkAutomatically ? `&moveTo=${playerPos.x},${playerPos.y}` : ""
}`;
}
function updateInputFieldValue() {
const input = document.getElementById("input-share-link");
if (input) {
(input as HTMLInputElement).value = getLink();
}
}
let canShare = navigator.share !== undefined;
async function shareLink() { async function shareLink() {
const shareData = { url: location.toString() }; const shareData = { url: getLink() };
try { try {
await navigator.share(shareData); await navigator.share(shareData);
@@ -22,29 +44,71 @@
<div class="guest-main"> <div class="guest-main">
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> {#if !canShare}
<h3>{$LL.menu.invite.description()}</h3> <section class="share-url not-mobile">
<input type="text" readonly id="input-share-link" value={location.toString()} /> <h3>{$LL.menu.invite.description()}</h3>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button> <input type="text" readonly id="input-share-link" class="link-url" value={location.toString()} />
</section> <button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
<section class="is-mobile"> </section>
<h3>{$LL.menu.invite.description()}</h3> {:else}
<input type="hidden" readonly id="input-share-link" value={location.toString()} /> <section class="is-mobile">
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button> <h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section>
{/if}
<h3>Select an entry point</h3>
<section class="nes-select is-dark starting-points">
<select
bind:value={entryPoint}
on:blur={() => {
updateInputFieldValue();
}}
>
{#each $startLayerNamesStore as entryPointName}
<option value={entryPointName}>{entryPointName}</option>
{/each}
</select>
</section> </section>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={walkAutomatically}
on:change={() => {
updateInputFieldValue();
}}
/>
<span>{$LL.menu.invite.walk_automatically_to_position()}</span>
</label>
</section> </section>
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.guest-main { div.guest-main {
width: 50%;
margin-left: auto;
margin-right: auto;
height: calc(100% - 56px); height: calc(100% - 56px);
text-align: center; input.link-url {
width: calc(100% - 200px);
}
.starting-points {
width: 80%;
}
section { section {
margin-bottom: 50px; margin-bottom: 50px;
} }
section.nes-select select:focus {
outline: none;
}
section.container-overflow { section.container-overflow {
height: 100%; height: 100%;
margin: 0; margin: 0;
@@ -53,25 +117,23 @@
} }
section.is-mobile { section.is-mobile {
display: none; display: block;
text-align: center;
margin-bottom: 20px;
} }
} }
@media only screen and (max-width: 900px), only screen and (max-height: 600px) { @include media-breakpoint-up(md) {
div.guest-main { div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow { section.container-overflow {
height: calc(100% - 120px); height: calc(100% - 120px);
} }
} }
} }
@include media-breakpoint-up(lg) {
div.guest-main {
width: 100%;
}
}
</style> </style>
+13 -6
View File
@@ -73,6 +73,7 @@
} else { } else {
const customMenu = customMenuIframe.get(menu.label); const customMenu = customMenuIframe.get(menu.label);
if (customMenu !== undefined) { if (customMenu !== undefined) {
activeSubMenu = menu;
props = { url: customMenu.url, allowApi: customMenu.allowApi }; props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu; activeComponent = CustomSubMenu;
} else { } else {
@@ -129,6 +130,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container { .nes-container {
padding: 5px; padding: 5px;
} }
@@ -140,11 +143,15 @@
pointer-events: auto; pointer-events: auto;
height: 80%; height: 80%;
width: 75%; width: 75%;
top: 10%; top: 4%;
position: relative; left: 0;
z-index: 80; right: 0;
margin: auto; margin-left: auto;
margin-right: auto;
position: absolute;
z-index: 900;
display: grid; display: grid;
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid)); grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
@@ -177,12 +184,12 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
div.menu-container-main { div.menu-container-main {
--size-first-columns-grid: 120px; --size-first-columns-grid: 120px;
height: 70%; height: 70%;
top: 55px; top: 55px;
width: 100%; width: 95%;
font-size: 0.5em; font-size: 0.5em;
div.menu-nav-sidebar { div.menu-nav-sidebar {
+85 -48
View File
@@ -14,6 +14,7 @@
function showMenu() { function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore)); menuVisiblilityStore.set(!get(menuVisiblilityStore));
} }
function showChat() { function showChat() {
chatVisibilityStore.set(true); chatVisibilityStore.set(true);
} }
@@ -21,68 +22,104 @@
function register() { function register() {
window.open(`${ADMIN_URL}/second-step-register`, "_self"); window.open(`${ADMIN_URL}/second-step-register`, "_self");
} }
function showInvite() { function showInvite() {
showShareLinkMapModalStore.set(true); showShareLinkMapModalStore.set(true);
} }
function noDrag() {
return false;
}
</script> </script>
<svelte:window /> <svelte:window />
<main class="menuIcon"> <main class="menuIcon noselect">
{#if $limitMapStore} {#if $limitMapStore}
<span class="nes-btn is-dark"> <span class="nes-btn is-dark">
<img <img
src={logoInvite} src={logoInvite}
alt={$LL.menu.icon.open.invite()} alt={$LL.menu.icon.open.invite()}
class="nes-pointer" class="nes-pointer"
on:click|preventDefault={showInvite} draggable="false"
/> on:dragstart|preventDefault={noDrag}
</span> on:click|preventDefault={showInvite}
/>
<span class="nes-btn is-dark"> </span>
<img <span class="nes-btn is-dark">
src={logoRegister} <img
alt={$LL.menu.icon.open.register()} src={logoRegister}
class="nes-pointer" alt={$LL.menu.icon.open.register()}
on:click|preventDefault={register} class="nes-pointer"
/> draggable="false"
</span> on:dragstart|preventDefault={noDrag}
on:click|preventDefault={register}
/>
</span>
{:else} {:else}
<span class="nes-btn is-dark"> <span class="nes-btn is-dark">
<img src={logoWA} alt={$LL.menu.icon.open.menu()} class="nes-pointer" on:click|preventDefault={showMenu} /> <img
</span> src={logoWA}
<span class="nes-btn is-dark"> alt={$LL.menu.icon.open.menu()}
<img src={logoTalk} alt={$LL.menu.icon.open.chat()} class="nes-pointer" on:click|preventDefault={showChat} /> class="nes-pointer"
</span> draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showMenu}
/>
</span>
<span class="nes-btn is-dark">
<img
src={logoTalk}
alt={$LL.menu.icon.open.chat()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showChat}
/>
</span>
{/if} {/if}
</main> </main>
<style lang="scss"> <style lang="scss">
.menuIcon { @import "../../../style/breakpoints.scss";
display: inline-grid;
z-index: 90;
position: relative;
margin: 25px;
img {
pointer-events: auto;
width: 24px;
padding-top: 0;
margin: 3px
}
}
.menuIcon img:hover{
transform: scale(1.2);
}
@media only screen and (max-width: 800px),
only screen and (max-height: 800px) {
.menuIcon { .menuIcon {
margin: 6px; display: inline-grid;
z-index: 90;
img { position: relative;
width: 16px; margin: 25px;
} img {
pointer-events: auto;
width: 24px;
padding-top: 0;
margin: 3px
}
} }
}
.menuIcon img:hover {
transform: scale(1.2);
}
@include media-breakpoint-up(sm) {
.menuIcon {
margin-top: 10%;
img {
pointer-events: auto;
width: 60px;
padding-top: 0;
}
}
.menuIcon img:hover {
transform: scale(1.2);
}
}
@include media-breakpoint-up(md) {
.menuIcon {
img {
width: 50px;
}
}
}
</style> </style>
@@ -13,7 +13,6 @@
import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene"; import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore"; import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg"; import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
import btnProfileSubMenuIdentity from "../images/btn-menu-profile-identity.svg";
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg"; import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
import Woka from "../Woka/Woka.svelte"; import Woka from "../Woka/Woka.svelte";
import Companion from "../Companion/Companion.svelte"; import Companion from "../Companion/Companion.svelte";
@@ -30,12 +29,6 @@
gameManager.leaveGame(SelectCompanionSceneName, new SelectCompanionScene()); gameManager.leaveGame(SelectCompanionSceneName, new SelectCompanionScene());
} }
function openEditNameScene() {
disableMenuStores();
loginSceneVisibleStore.set(true);
gameManager.leaveGame(LoginSceneName, new LoginScene());
}
function openEditSkinScene() { function openEditSkinScene() {
disableMenuStores(); disableMenuStores();
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
@@ -104,6 +97,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.customize-main { div.customize-main {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
@@ -163,7 +158,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
div.customize-main.content section button { div.customize-main.content section button {
width: 130px; width: 130px;
} }
@@ -2,11 +2,11 @@
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import { videoConstraintStore } from "../../Stores/MediaStore"; import { videoConstraintStore } from "../../Stores/MediaStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { menuVisiblilityStore } from "../../Stores/MenuStore"; import { menuVisiblilityStore } from "../../Stores/MenuStore";
import LL, { locale } from "../../i18n/i18n-svelte"; import LL, { locale } from "../../i18n/i18n-svelte";
import type { Locales } from "../../i18n/i18n-types"; import type { Locales } from "../../i18n/i18n-types";
import { displayableLocales, setCurrentLocale } from "../../i18n/locales"; import { displayableLocales, setCurrentLocale } from "../../i18n/locales";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
let fullscreen: boolean = localUserStore.getFullscreen(); let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted"; let notification: boolean = localUserStore.getNotification() === "granted";
@@ -85,6 +85,8 @@
function closeMenu() { function closeMenu() {
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
} }
const isMobile = isMediaBreakpointUp("md");
</script> </script>
<div class="settings-main" on:submit|preventDefault={saveSetting}> <div class="settings-main" on:submit|preventDefault={saveSetting}>
@@ -93,22 +95,22 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueGame}> <select bind:value={valueGame}>
<option value={120} <option value={120}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.high() ? $LL.menu.settings.gameQuality.short.high()
: $LL.menu.settings.gameQuality.long.high()}</option : $LL.menu.settings.gameQuality.long.high()}</option
> >
<option value={60} <option value={60}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.medium() ? $LL.menu.settings.gameQuality.short.medium()
: $LL.menu.settings.gameQuality.long.medium()}</option : $LL.menu.settings.gameQuality.long.medium()}</option
> >
<option value={40} <option value={40}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.small() ? $LL.menu.settings.gameQuality.short.small()
: $LL.menu.settings.gameQuality.long.small()}</option : $LL.menu.settings.gameQuality.long.small()}</option
> >
<option value={20} <option value={20}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.minimum() ? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option : $LL.menu.settings.gameQuality.long.minimum()}</option
> >
@@ -120,22 +122,22 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueVideo}> <select bind:value={valueVideo}>
<option value={30} <option value={30}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.high() ? $LL.menu.settings.videoQuality.short.high()
: $LL.menu.settings.videoQuality.long.high()}</option : $LL.menu.settings.videoQuality.long.high()}</option
> >
<option value={20} <option value={20}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.medium() ? $LL.menu.settings.videoQuality.short.medium()
: $LL.menu.settings.videoQuality.long.medium()}</option : $LL.menu.settings.videoQuality.long.medium()}</option
> >
<option value={10} <option value={10}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.small() ? $LL.menu.settings.videoQuality.short.small()
: $LL.menu.settings.videoQuality.long.small()}</option : $LL.menu.settings.videoQuality.long.small()}</option
> >
<option value={5} <option value={5}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.minimum() ? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option : $LL.menu.settings.videoQuality.long.minimum()}</option
> >
@@ -199,6 +201,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.settings-main { div.settings-main {
height: calc(100% - 40px); height: calc(100% - 40px);
overflow-y: auto; overflow-y: auto;
@@ -236,7 +240,7 @@
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @include media-breakpoint-up(md) {
div.settings-main { div.settings-main {
section { section {
padding: 0; padding: 0;
@@ -2,9 +2,7 @@
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import type { World } from "../../Connexion/World"; import type { World } from "../../Connexion/World";
let worlds : Promise<World[]> = connectionManager.getWorlds();
let worlds = connectionManager.getWorlds();
function worldRoomId(world: World) { function worldRoomId(world: World) {
return world.roomId; return world.roomId;
@@ -32,6 +32,7 @@
max-width: 80vw; max-width: 80vw;
overflow: auto; overflow: auto;
text-align: center; text-align: center;
z-index: 500;
h2 { h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
@@ -75,6 +75,7 @@
max-width: 80vw; max-width: 80vw;
overflow: auto; overflow: auto;
text-align: center; text-align: center;
z-index: 450;
h2 { h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
+69 -10
View File
@@ -2,7 +2,7 @@
import { obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { localStreamStore, isSilentStore } from "../Stores/MediaStore"; import { localStreamStore, isSilentStore } from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte"; import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy } from "svelte"; import { onDestroy, onMount } from "svelte";
import { srcObject } from "./Video/utils"; import { srcObject } from "./Video/utils";
import LL from "../i18n/i18n-svelte"; import LL from "../i18n/i18n-svelte";
@@ -23,16 +23,75 @@
isSilent = value; isSilent = value;
}); });
let cameraContainer: HTMLDivElement;
onMount(() => {
cameraContainer.addEventListener("transitionend", () => {
if (cameraContainer.classList.contains("hide")) {
cameraContainer.style.visibility = "hidden";
}
});
cameraContainer.addEventListener("transitionstart", () => {
if (!cameraContainer.classList.contains("hide")) {
cameraContainer.style.visibility = "visible";
}
});
});
onDestroy(unsubscribeIsSilent); onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div
<div class="video-container nes-container is-rounded is-dark div-myCamVideo" class="nes-container is-rounded my-cam-video-container"
class:hide={!$obtainedMediaConstraintStore.video || isSilent}> class:hide={($localStreamStore.type !== "success" || !$obtainedMediaConstraintStore.video) && !isSilent}
{#if $localStreamStore.type === "success" && $localStreamStore.stream} bind:this={cameraContainer}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline /> >
<SoundMeterWidget {stream} /> {#if isSilent}
{/if} <div class="is-silent">{$LL.camera.my.silentZone()}</div>
</div> {:else if $localStreamStore.type === "success" && $localStreamStore.stream}
<div class="nes-container is-dark is-silent" class:hide={isSilent}>{$LL.camera.my.silentZone()}</div> <video class="my-cam-video" use:srcObject={stream} autoplay muted playsinline />
<SoundMeterWidget {stream} />
{/if}
</div> </div>
<style lang="scss">
@import "../../style/breakpoints.scss";
.my-cam-video-container {
position: absolute;
right: 15px;
bottom: 30px;
max-height: 20%;
transition: transform 1000ms;
padding: 0;
background-color: rgba(#000000, 0.6);
background-clip: content-box;
overflow: hidden;
line-height: 0;
z-index: 250;
&.nes-container.is-rounded {
border-image-outset: 1;
}
}
.my-cam-video-container.hide {
transform: translateX(200%);
}
.my-cam-video {
background-color: #00000099;
max-height: 20vh;
max-width: max(25vw, 150px);
width: 100%;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.is-silent {
font-size: 2em;
color: white;
padding: 40px 20px;
}
</style>
@@ -108,12 +108,16 @@
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
z-index: 650;
position: relative; position: absolute;
height: 70vh; height: 70vh;
width: 50vw; width: 50vw;
top: 10vh; top: 4%;
margin: auto;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
section.report-menu-title { section.report-menu-title {
display: grid; display: grid;
@@ -137,13 +141,4 @@
display: none; display: none;
} }
} }
@media only screen and (max-width: 800px) {
div.report-menu-main {
top: 21vh;
height: 60vh;
width: 100vw;
font-size: 0.5em;
}
}
</style> </style>
@@ -47,6 +47,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
form.selectCompanionScene { form.selectCompanionScene {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; pointer-events: auto;
@@ -85,7 +87,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
form.selectCompanionScene button.selectCharacterButtonLeft { form.selectCompanionScene button.selectCharacterButtonLeft {
left: 5vw; left: 5vw;
} }
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { fly, fade } from "svelte/transition"; import { fly, fade } from "svelte/transition";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore"; import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore"; import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@@ -13,6 +14,8 @@
onMount(() => { onMount(() => {
timeToRead(); timeToRead();
const gameScene = gameManager.getCurrentGameScene();
gameScene.playSound("audio-report-message");
}); });
function timeToRead() { function timeToRead() {
@@ -53,18 +56,19 @@
on:click|preventDefault={closeBanMessage}>{nameButton}</button on:click|preventDefault={closeBanMessage}>{nameButton}</button
> >
</div> </div>
<!-- svelte-ignore a11y-media-has-caption -->
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3" />
</audio>
</div> </div>
<style lang="scss"> <style lang="scss">
div.main-ban-message { div.main-ban-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: absolute;
top: 15vh; top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 850;
height: 70vh; height: 70vh;
width: 60vw; width: 60vw;
@@ -11,3 +11,9 @@
</div> </div>
{/each} {/each}
</div> </div>
<style lang="scss">
.main-ban-message-container {
z-index: 800;
}
</style>
@@ -42,14 +42,17 @@
div.main-text-message { div.main-text-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute;
max-height: 25vh; max-height: 25%;
width: 80vw; width: 60%;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
margin-bottom: 16px; top: 6%;
margin-top: 0; left: 0;
right: 0;
padding-bottom: 0; padding-bottom: 0;
z-index: 240;
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;
@@ -15,7 +15,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
div.main-text-message-container { .main-text-message-container {
padding-top: 16px; padding-top: 16px;
z-index: 800;
} }
</style> </style>
@@ -37,6 +37,7 @@
background-color: black; background-color: black;
border-radius: 30px 0 0 30px; border-radius: 30px 0 0 30px;
display: inline-flex; display: inline-flex;
z-index: 750;
img { img {
border-radius: 50%; border-radius: 50%;
+7 -1
View File
@@ -25,11 +25,17 @@
<style lang="scss"> <style lang="scss">
div.error-div { div.error-div {
pointer-events: auto; pointer-events: auto;
margin-top: 10vh; margin-top: 4%;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
left: 0;
right: 0;
position: absolute;
width: max-content; width: max-content;
max-width: 80vw; max-width: 80vw;
z-index: 230;
height: auto !important;
background-clip: padding-box;
.button-bar { .button-bar {
text-align: center; text-align: center;
@@ -1,15 +1,33 @@
<script lang="typescript"> <script lang="typescript">
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore"; import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore"; import type { Streamable } from "../../Stores/StreamableCollectionStore";
import { srcObject } from "./utils"; import { srcObject } from "./utils";
export let clickable = false;
export let peer: ScreenSharingLocalMedia; export let peer: ScreenSharingLocalMedia;
let stream = peer.stream; let stream = peer.stream;
export let cssClass: string | undefined; export let cssClass: string | undefined;
let embedScreen: EmbedScreen;
if (stream) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
</script> </script>
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}> <div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
{#if stream} {#if stream}
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <video
use:srcObject={stream}
autoplay
muted
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
{/if} {/if}
</div> </div>
+109 -8
View File
@@ -7,14 +7,115 @@
import type { Streamable } from "../../Stores/StreamableCollectionStore"; import type { Streamable } from "../../Stores/StreamableCollectionStore";
export let streamable: Streamable; export let streamable: Streamable;
export let isHightlighted = false;
export let isClickable = false;
export let mozaicSolo = false;
export let mozaicFullWidth = false;
export let mozaicQuarter = false;
</script> </script>
<div class="media-container"> <div
{#if streamable instanceof VideoPeer} class="media-container nes-container is-rounded {isHightlighted ? 'hightlighted' : ''}"
<VideoMediaBox peer={streamable} /> class:clickable={isClickable}
{:else if streamable instanceof ScreenSharingPeer} class:mozaic-solo={mozaicSolo}
<ScreenSharingMediaBox peer={streamable} /> class:mozaic-full-width={mozaicFullWidth}
{:else} class:mozaic-quarter={mozaicQuarter}
<LocalStreamMediaBox peer={streamable} cssClass="" /> >
{/if} <div>
{#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable} clickable={isClickable} />
{:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable} clickable={isClickable} />
{:else}
<LocalStreamMediaBox peer={streamable} clickable={isClickable} cssClass="" />
{/if}
</div>
</div> </div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.media-container {
display: flex;
margin-top: 4%;
margin-bottom: 4%;
margin-left: auto;
margin-right: auto;
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s,
max-width 0.2s;
pointer-events: auto;
padding: 0;
max-height: 200px;
max-width: 85%;
&:hover {
margin-top: 2%;
margin-bottom: 2%;
}
&.hightlighted {
margin-top: 0% !important;
margin-bottom: 0% !important;
margin-left: 0% !important;
max-height: 100% !important;
max-width: 96% !important;
&:hover {
margin-top: 0% !important;
margin-bottom: 0% !important;
}
}
&.mozaic-solo {
max-height: inherit !important;
width: 90% !important;
}
&.mozaic-full-width {
width: 95%;
max-width: 95%;
margin-left: 3%;
margin-right: 3%;
margin-top: auto;
margin-bottom: auto;
max-height: 95%;
&:hover {
margin-top: auto;
margin-bottom: auto;
}
}
&.mozaic-quarter {
width: 95%;
max-width: 95%;
margin-top: auto;
margin-bottom: auto;
max-height: 95%;
&:hover {
margin-top: auto;
margin-bottom: auto;
}
}
&.nes-container.is-rounded {
border-image-outset: 1;
}
> div {
background-color: rgba(0, 0, 0, 0.6);
display: flex;
width: 100%;
}
}
@include media-breakpoint-only(md) {
.media-container {
margin-top: 10%;
margin-bottom: 10%;
}
}
</style>
@@ -1,26 +0,0 @@
<script lang="ts">
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { afterUpdate } from "svelte";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte";
afterUpdate(() => {
biggestAvailableAreaStore.recompute();
});
</script>
<div class="main-section">
{#if $videoFocusStore}
{#key $videoFocusStore.uniqueId}
<MediaBox streamable={$videoFocusStore} />
{/key}
{/if}
</div>
<aside class="sidebar">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if peer !== $videoFocusStore}
<MediaBox streamable={peer} />
{/if}
{/each}
</aside>
@@ -1,12 +1,26 @@
<script lang="ts"> <script lang="ts">
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer"; import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { getColorByString, srcObject } from "./utils"; import { getColorByString, srcObject } from "./utils";
export let clickable = false;
export let peer: ScreenSharingPeer; export let peer: ScreenSharingPeer;
let streamStore = peer.streamStore; let streamStore = peer.streamStore;
let name = peer.userName; let name = peer.userName;
let statusStore = peer.statusStore; let statusStore = peer.statusStore;
let embedScreen: EmbedScreen;
if (peer) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
</script> </script>
<div class="video-container"> <div class="video-container">
@@ -16,11 +30,17 @@
{#if $statusStore === "error"} {#if $statusStore === "error"}
<div class="rtc-error" /> <div class="rtc-error" />
{/if} {/if}
{#if $streamStore === null} {#if $streamStore !== null}
<i style="background-color: {getColorByString(name)};">{name}</i> <i class="container">
{:else} <span style="background-color: {getColorByString(name)};">{name}</span>
</i>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <video
use:srcObject={$streamStore}
autoplay
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
{/if} {/if}
</div> </div>
@@ -29,5 +49,10 @@
video { video {
width: 100%; width: 100%;
} }
i {
span {
padding: 2px 32px;
}
}
} }
</style> </style>
+61 -24
View File
@@ -4,11 +4,17 @@
import microphoneCloseImg from "../images/microphone-close.svg"; import microphoneCloseImg from "../images/microphone-close.svg";
import reportImg from "./images/report.svg"; import reportImg from "./images/report.svg";
import blockSignImg from "./images/blockSign.svg"; import blockSignImg from "./images/blockSign.svg";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { getColorByString, srcObject } from "./utils"; import { getColorByString, srcObject } from "./utils";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import Woka from "../Woka/Woka.svelte"; import Woka from "../Woka/Woka.svelte";
import { onMount } from "svelte";
import { isMediaBreakpointOnly } from "../../Utils/BreakpointsUtils";
export let clickable = false;
export let peer: VideoPeer; export let peer: VideoPeer;
let streamStore = peer.streamStore; let streamStore = peer.streamStore;
@@ -19,9 +25,37 @@
function openReport(peer: VideoPeer): void { function openReport(peer: VideoPeer): void {
showReportScreenStore.set({ userId: peer.userId, userName: peer.userName }); showReportScreenStore.set({ userId: peer.userId, userName: peer.userName });
} }
let embedScreen: EmbedScreen;
let videoContainer: HTMLDivElement;
let minimized = isMediaBreakpointOnly("md");
if (peer) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
function noDrag() {
return false;
}
const resizeObserver = new ResizeObserver(() => {
minimized = isMediaBreakpointOnly("md");
});
onMount(() => {
resizeObserver.observe(videoContainer);
});
</script> </script>
<div class="video-container nes-container is-rounded is-dark"> <div
class="video-container"
class:no-clikable={!clickable}
bind:this={videoContainer}
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
>
{#if $statusStore === "connecting"} {#if $statusStore === "connecting"}
<div class="connecting-spinner" /> <div class="connecting-spinner" />
{/if} {/if}
@@ -29,43 +63,46 @@
<div class="rtc-error" /> <div class="rtc-error" />
{/if} {/if}
<!-- {#if !$constraintStore || $constraintStore.video === false} --> <!-- {#if !$constraintStore || $constraintStore.video === false} -->
<i <i class="container">
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}" <span style="background-color: {getColorByString(name)};">{name}</span>
style="background-color: {getColorByString(name)};"
>
<span>{peer.userName}</span>
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
</i> </i>
<div class="woka-icon {($constraintStore && $constraintStore.video !== false) || minimized ? '' : 'no-video'}">
<Woka userId={peer.userId} placeholderSrc={""} />
</div>
<!-- {/if} --> <!-- {/if} -->
{#if $constraintStore && $constraintStore.audio === false} {#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} class="active" alt="Muted" /> <img
src={microphoneCloseImg}
class="active noselect"
draggable="false"
on:dragstart|preventDefault={noDrag}
alt="Muted"
/>
{/if} {/if}
<button class="report nes-button is-dark" on:click={() => openReport(peer)}> <button class="report nes-button is-dark" on:click={() => openReport(peer)}>
<img alt="Report this user" src={reportImg} /> <img alt="Report this user" draggable="false" on:dragstart|preventDefault={noDrag} src={reportImg} />
<span>Report/Block</span> <span class="noselect">Report/Block</span>
</button> </button>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <video
<img src={blockSignImg} class="block-logo" alt="Block" /> class:no-video={!$constraintStore || $constraintStore.video === false}
use:srcObject={$streamStore}
autoplay
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
<img src={blockSignImg} draggable="false" on:dragstart|preventDefault={noDrag} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false} {#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore} /> <SoundMeterWidget stream={$streamStore} />
{/if} {/if}
</div> </div>
<style> <style lang="scss">
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 15px;
} }
video.no-video {
.minimized { visibility: collapse;
left: auto;
transform: scale(0.5);
opacity: 0.5;
}
.woka-icon {
margin-right: 3px;
} }
</style> </style>
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { LayoutMode } from "../../WebRtc/LayoutManager"; // import {LayoutMode} from "../../WebRtc/LayoutManager";
import { layoutModeStore } from "../../Stores/StreamableCollectionStore"; // import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
import PresentationLayout from "./PresentationLayout.svelte"; // import PresentationLayout from "./PresentationLayout.svelte";
import ChatLayout from "./ChatLayout.svelte"; // import ChatLayout from "./ChatLayout.svelte";
</script> </script>
<div class="video-overlay"> <div class="video-overlay">
{#if $layoutModeStore === LayoutMode.Presentation} <!-- {#if $layoutModeStore === LayoutMode.Presentation }
<PresentationLayout /> <PresentationLayout />
{:else} {:else}
<ChatLayout /> <ChatLayout />
{/if} {/if} -->
</div> </div>
<style lang="scss"> <style lang="scss">
@@ -57,6 +57,7 @@
height: 120px; height: 120px;
margin: auto; margin: auto;
animation: spin 2s linear infinite; animation: spin 2s linear infinite;
z-index: 350;
} }
@keyframes spin { @keyframes spin {
@@ -27,18 +27,21 @@
<style lang="scss"> <style lang="scss">
main.warningMain { main.warningMain {
pointer-events: auto; pointer-events: auto;
width: 100vw; width: 80%;
background-color: #f9e81e; background-color: #f9e81e;
color: #14304c; color: #14304c;
text-align: center; text-align: center;
position: absolute; position: absolute;
left: 50%;
transform: translate(-50%, 0); top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
font-family: Lato; font-family: Lato;
min-width: 300px; min-width: 300px;
opacity: 0.9; opacity: 0.9;
z-index: 2; z-index: 700;
h2 { h2 {
padding: 5px; padding: 5px;
} }
+12 -1
View File
@@ -22,10 +22,21 @@
src = source ?? placeholderSrc; src = source ?? placeholderSrc;
}); });
function noDrag() {
return false;
}
onDestroy(unsubscribe); onDestroy(unsubscribe);
</script> </script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" /> <img
{src}
alt=""
class="nes-pointer noselect"
style="--theme-width: {width}; --theme-height: {height}"
draggable="false"
on:dragstart|preventDefault={noDrag}
/>
<style> <style>
img { img {
@@ -49,6 +49,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
form.selectCharacterScene { form.selectCharacterScene {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; pointer-events: auto;
@@ -91,7 +93,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
form.selectCharacterScene button.selectCharacterButtonLeft { form.selectCharacterScene button.selectCharacterButtonLeft {
left: 5vw; left: 5vw;
} }
+6 -5
View File
@@ -20,6 +20,7 @@ import { showLimitRoomModalStore } from "../Stores/ModalStore";
import { locales } from "../i18n/i18n-util"; import { locales } from "../i18n/i18n-util";
import type { Locales } from "../i18n/i18n-types"; import type { Locales } from "../i18n/i18n-types";
import { setCurrentLocale } from "../i18n/locales"; import { setCurrentLocale } from "../i18n/locales";
import type { World } from "./World";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@@ -187,14 +188,14 @@ class ConnectionManager {
window.location.hash; window.location.hash;
} }
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value
await localUserStore.setLastRoomUrl(new URL(roomPath).href);
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
//before set token of user we must load room and all information. For example the mandatory authentication could be require on current room //before set token of user we must load room and all information. For example the mandatory authentication could be require on current room
this._currentRoom = await Room.createRoom(new URL(roomPath)); this._currentRoom = await Room.createRoom(new URL(roomPath));
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value
await localUserStore.setLastRoomUrl(this._currentRoom.href);
//todo: add here some kind of warning if authToken has expired. //todo: add here some kind of warning if authToken has expired.
if (!this.authToken && !this._currentRoom.authenticationMandatory) { if (!this.authToken && !this._currentRoom.authenticationMandatory) {
await this.anonymousLogin(); await this.anonymousLogin();
@@ -384,7 +385,7 @@ class ConnectionManager {
userIsConnected.set(true); userIsConnected.set(true);
} }
async getWorlds() { async getWorlds() : Promise<World[]> {
const token = localUserStore.getAuthToken(); const token = localUserStore.getAuthToken();
if (!token) { if (!token) {
throw new Error("No token provided"); throw new Error("No token provided");
+41 -28
View File
@@ -1,33 +1,46 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; declare global {
const START_ROOM_URL: string = interface Window {
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json"; env?: Record<string, string>;
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost"; }
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re"; }
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const ICON_URL = process.env.ICON_URL || "//icon.workadventure.localhost"; const getEnv = (key: string): string | undefined => {
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; if (global.window?.env) {
const TURN_SERVER: string = process.env.TURN_SERVER || ""; return global.window.env[key];
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true"; }
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true"; if (global.process?.env) {
const TURN_USER: string = process.env.TURN_USER || ""; return global.process.env[key];
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ""; }
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; return;
const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true"; };
const DEBUG_MODE: boolean = getEnv("DEBUG_MODE") == "true";
const START_ROOM_URL: string = getEnv("START_ROOM_URL") || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const PUSHER_URL = getEnv("PUSHER_URL") || "//pusher.workadventure.localhost";
export const ADMIN_URL = getEnv("ADMIN_URL") || "//workadventu.re";
const UPLOADER_URL = getEnv("UPLOADER_URL") || "//uploader.workadventure.localhost";
const ICON_URL = getEnv("ICON_URL") || "//icon.workadventure.localhost";
const STUN_SERVER: string = getEnv("STUN_SERVER") || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = getEnv("TURN_SERVER") || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = getEnv("SKIP_RENDER_OPTIMIZATIONS") == "true";
const DISABLE_NOTIFICATIONS: boolean = getEnv("DISABLE_NOTIFICATIONS") == "true";
const TURN_USER: string = getEnv("TURN_USER") || "";
const TURN_PASSWORD: string = getEnv("TURN_PASSWORD") || "";
const JITSI_URL: string | undefined = getEnv("JITSI_URL") === "" ? undefined : getEnv("JITSI_URL");
const JITSI_PRIVATE_MODE: boolean = getEnv("JITSI_PRIVATE_MODE") == "true";
const POSITION_DELAY = 200; // Wait 200ms between sending position events const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8; export const MAX_USERNAME_LENGTH = parseInt(getEnv("MAX_USERNAME_LENGTH") || "") || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export const MAX_PER_GROUP = parseInt(getEnv("MAX_PER_GROUP") || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true"; export const DISPLAY_TERMS_OF_USE = getEnv("DISPLAY_TERMS_OF_USE") == "true";
export const NODE_ENV = process.env.NODE_ENV || "development"; export const NODE_ENV = getEnv("NODE_ENV") || "development";
export const CONTACT_URL = process.env.CONTACT_URL || undefined; export const CONTACT_URL = getEnv("CONTACT_URL") || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined; export const PROFILE_URL = getEnv("PROFILE_URL") || undefined;
export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || ""; export const POSTHOG_API_KEY: string = (getEnv("POSTHOG_API_KEY") as string) || "";
export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; export const POSTHOG_URL = getEnv("POSTHOG_URL") || undefined;
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true"; export const DISABLE_ANONYMOUS: boolean = getEnv("DISABLE_ANONYMOUS") === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; export const OPID_LOGIN_SCREEN_PROVIDER = getEnv("OPID_LOGIN_SCREEN_PROVIDER");
const FALLBACK_LOCALE = process.env.FALLBACK_LOCALE || undefined; const FALLBACK_LOCALE = getEnv("FALLBACK_LOCALE") || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export { export {
DEBUG_MODE, DEBUG_MODE,
@@ -9,4 +9,7 @@ export interface UserInputHandlerInterface {
handlePointerUpEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void; handlePointerUpEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handlePointerDownEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void; handlePointerDownEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handleSpaceKeyUpEvent: (event: Event) => Event; handleSpaceKeyUpEvent: (event: Event) => Event;
addSpaceEventListener: (callback: Function) => void;
removeSpaceEventListner: (callback: Function) => void;
} }
+5 -2
View File
@@ -4,6 +4,7 @@ import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Anima
import { TexturesHelper } from "../Helpers/TexturesHelper"; import { TexturesHelper } from "../Helpers/TexturesHelper";
import { Writable, writable } from "svelte/store"; import { Writable, writable } from "svelte/store";
import type { PictureStore } from "../../Stores/PictureStore"; import type { PictureStore } from "../../Stores/PictureStore";
import type CancelablePromise from "cancelable-promise";
export interface CompanionStatus { export interface CompanionStatus {
x: number; x: number;
@@ -25,8 +26,9 @@ export class Companion extends Container {
private direction: PlayerAnimationDirections; private direction: PlayerAnimationDirections;
private animationType: PlayerAnimationTypes; private animationType: PlayerAnimationTypes;
private readonly _pictureStore: Writable<string | undefined>; private readonly _pictureStore: Writable<string | undefined>;
private texturePromise: CancelablePromise<string | void> | undefined;
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) { constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: CancelablePromise<string>) {
super(scene, x + 14, y + 4); super(scene, x + 14, y + 4);
this.sprites = new Map<string, Sprite>(); this.sprites = new Map<string, Sprite>();
@@ -41,7 +43,7 @@ export class Companion extends Container {
this.companionName = name; this.companionName = name;
this._pictureStore = writable(undefined); this._pictureStore = writable(undefined);
texturePromise this.texturePromise = texturePromise
.then((resource) => { .then((resource) => {
this.addResource(resource); this.addResource(resource);
this.invisible = false; this.invisible = false;
@@ -234,6 +236,7 @@ export class Companion extends Container {
} }
public destroy(): void { public destroy(): void {
this.texturePromise?.cancel();
for (const sprite of this.sprites.values()) { for (const sprite of this.sprites.values()) {
if (this.scene) { if (this.scene) {
this.scene.sys.updateList.remove(sprite); this.scene.sys.updateList.remove(sprite);
@@ -1,5 +1,6 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./CompanionTextures"; import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./CompanionTextures";
import CancelablePromise from "cancelable-promise";
export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => { export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => {
COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => { COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => {
@@ -9,8 +10,12 @@ export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourc
return COMPANION_RESOURCES; return COMPANION_RESOURCES;
}; };
export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise<string> => { export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): CancelablePromise<string> => {
return new Promise((resolve, reject) => { return new CancelablePromise((resolve, reject, cancel) => {
cancel(() => {
return;
});
const resource = COMPANION_RESOURCES.find((item) => item.name === name); const resource = COMPANION_RESOURCES.find((item) => item.name === name);
if (typeof resource === "undefined") { if (typeof resource === "undefined") {
@@ -63,8 +63,6 @@ export class SoundMeter {
//this.slow = 0.95 * that.slow + 0.05 * that.instant; //this.slow = 0.95 * that.slow + 0.05 * that.instant;
//this.clip = clipcount / input.length; //this.clip = clipcount / input.length;
//console.log('instant', this.instant, 'clip', this.clip);
return this.instant; return this.instant;
} }
+102 -33
View File
@@ -15,6 +15,8 @@ import { TexturesHelper } from "../Helpers/TexturesHelper";
import type { PictureStore } from "../../Stores/PictureStore"; import type { PictureStore } from "../../Stores/PictureStore";
import { Unsubscriber, Writable, writable } from "svelte/store"; import { Unsubscriber, Writable, writable } from "svelte/store";
import { createColorStore } from "../../Stores/OutlineColorStore"; import { createColorStore } from "../../Stores/OutlineColorStore";
import type { OutlineableInterface } from "../Game/OutlineableInterface";
import type CancelablePromise from "cancelable-promise";
const playerNameY = -25; const playerNameY = -25;
@@ -28,14 +30,15 @@ interface AnimationData {
const interactiveRadius = 35; const interactiveRadius = 35;
export abstract class Character extends Container { export abstract class Character extends Container implements OutlineableInterface {
private bubble: SpeechBubble | null = null; private bubble: SpeechBubble | null = null;
private readonly playerName: Text; private readonly playerNameText: Text;
public PlayerValue: string; public playerName: string;
public sprites: Map<string, Sprite>; public sprites: Map<string, Sprite>;
protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
//private teleportation: Sprite; //private teleportation: Sprite;
private invisible: boolean; private invisible: boolean;
private clickable: boolean;
public companion?: Companion; public companion?: Companion;
private emote: Phaser.GameObjects.Text | null = null; private emote: Phaser.GameObjects.Text | null = null;
private emoteTween: Phaser.Tweens.Tween | null = null; private emoteTween: Phaser.Tweens.Tween | null = null;
@@ -43,30 +46,32 @@ export abstract class Character extends Container {
private readonly _pictureStore: Writable<string | undefined>; private readonly _pictureStore: Writable<string | undefined>;
private readonly outlineColorStore = createColorStore(); private readonly outlineColorStore = createColorStore();
private readonly outlineColorStoreUnsubscribe: Unsubscriber; private readonly outlineColorStoreUnsubscribe: Unsubscriber;
private texturePromise: CancelablePromise<string[] | void> | undefined;
constructor( constructor(
scene: GameScene, scene: GameScene,
x: number, x: number,
y: number, y: number,
texturesPromise: Promise<string[]>, texturesPromise: CancelablePromise<string[]>,
name: string, name: string,
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
frame: string | number, frame: string | number,
isClickable: boolean, isClickable: boolean,
companion: string | null, companion: string | null,
companionTexturePromise?: Promise<string> companionTexturePromise?: CancelablePromise<string>
) { ) {
super(scene, x, y /*, texture, frame*/); super(scene, x, y /*, texture, frame*/);
this.scene = scene; this.scene = scene;
this.PlayerValue = name; this.playerName = name;
this.invisible = true; this.invisible = true;
this.clickable = false;
this.sprites = new Map<string, Sprite>(); this.sprites = new Map<string, Sprite>();
this._pictureStore = writable(undefined); this._pictureStore = writable(undefined);
//textures are inside a Promise in case they need to be lazyloaded before use. //textures are inside a Promise in case they need to be lazyloaded before use.
texturesPromise this.texturePromise = texturesPromise
.then((textures) => { .then((textures) => {
this.addTextures(textures, frame); this.addTextures(textures, frame);
this.invisible = false; this.invisible = false;
@@ -81,9 +86,12 @@ export abstract class Character extends Container {
this.invisible = false; this.invisible = false;
this.playAnimation(direction, moving); this.playAnimation(direction, moving);
}); });
})
.finally(() => {
this.texturePromise = undefined;
}); });
this.playerName = new Text(scene, 0, playerNameY, name, { this.playerNameText = new Text(scene, 0, playerNameY, name, {
fontFamily: '"Press Start 2P"', fontFamily: '"Press Start 2P"',
fontSize: "8px", fontSize: "8px",
strokeThickness: 2, strokeThickness: 2,
@@ -94,30 +102,17 @@ export abstract class Character extends Container {
fontSize: 35, fontSize: 35,
}, },
}); });
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX); this.playerNameText.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName); this.add(this.playerNameText);
if (isClickable) { this.setClickable(isClickable);
this.setInteractive({
hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius),
hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
useHandCursor: true,
});
this.on("pointerover", () => {
this.outlineColorStore.pointerOver();
});
this.on("pointerout", () => {
this.outlineColorStore.pointerOut();
});
}
this.outlineColorStoreUnsubscribe = this.outlineColorStore.subscribe((color) => { this.outlineColorStoreUnsubscribe = this.outlineColorStore.subscribe((color) => {
if (color === undefined) { if (color === undefined) {
this.getOutlinePlugin()?.remove(this.playerName); this.getOutlinePlugin()?.remove(this.playerNameText);
} else { } else {
this.getOutlinePlugin()?.remove(this.playerName); this.getOutlinePlugin()?.remove(this.playerNameText);
this.getOutlinePlugin()?.add(this.playerName, { this.getOutlinePlugin()?.add(this.playerNameText, {
thickness: 2, thickness: 2,
outlineColor: color, outlineColor: color,
}); });
@@ -140,6 +135,55 @@ export abstract class Character extends Container {
} }
} }
public setClickable(clickable: boolean = true): void {
if (this.clickable === clickable) {
return;
}
this.clickable = clickable;
if (clickable) {
this.setInteractive({
hitArea: new Phaser.Geom.Circle(8, 8, interactiveRadius),
hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
useHandCursor: true,
});
return;
}
this.disableInteractive();
}
public isClickable() {
return this.clickable;
}
public getPosition(): { x: number; y: number } {
return { x: this.x, y: this.y };
}
/**
* Returns position based on where player is currently facing
* @param shift How far from player should the point of interest be.
*/
public getDirectionalActivationPosition(shift: number): { x: number; y: number } {
switch (this.lastDirection) {
case PlayerAnimationDirections.Down: {
return { x: this.x, y: this.y + shift };
}
case PlayerAnimationDirections.Left: {
return { x: this.x - shift, y: this.y };
}
case PlayerAnimationDirections.Right: {
return { x: this.x + shift, y: this.y };
}
case PlayerAnimationDirections.Up: {
return { x: this.x, y: this.y - shift };
}
}
}
public getObjectToOutline(): Phaser.GameObjects.GameObject {
return this.playerNameText;
}
private async getSnapshot(): Promise<string> { private async getSnapshot(): Promise<string> {
const sprites = Array.from(this.sprites.values()).map((sprite) => { const sprites = Array.from(this.sprites.values()).map((sprite) => {
return { sprite, frame: 1 }; return { sprite, frame: 1 };
@@ -156,7 +200,7 @@ export abstract class Character extends Container {
}); });
} }
public addCompanion(name: string, texturePromise?: Promise<string>): void { public addCompanion(name: string, texturePromise?: CancelablePromise<string>): void {
if (typeof texturePromise !== "undefined") { if (typeof texturePromise !== "undefined") {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
} }
@@ -326,6 +370,7 @@ export abstract class Character extends Container {
this.scene.sys.updateList.remove(sprite); this.scene.sys.updateList.remove(sprite);
} }
} }
this.texturePromise?.cancel();
this.list.forEach((objectContaining) => objectContaining.destroy()); this.list.forEach((objectContaining) => objectContaining.destroy());
this.outlineColorStoreUnsubscribe(); this.outlineColorStoreUnsubscribe();
super.destroy(); super.destroy();
@@ -405,18 +450,42 @@ export abstract class Character extends Container {
private destroyEmote() { private destroyEmote() {
this.emote?.destroy(); this.emote?.destroy();
this.emote = null; this.emote = null;
this.playerName.setVisible(true); this.playerNameText.setVisible(true);
} }
public get pictureStore(): PictureStore { public get pictureStore(): PictureStore {
return this._pictureStore; return this._pictureStore;
} }
public setOutlineColor(color: number): void { public setFollowOutlineColor(color: number): void {
this.outlineColorStore.setColor(color); this.outlineColorStore.setFollowColor(color);
} }
public removeOutlineColor(): void { public removeFollowOutlineColor(): void {
this.outlineColorStore.removeColor(); this.outlineColorStore.removeFollowColor();
}
public setApiOutlineColor(color: number): void {
this.outlineColorStore.setApiColor(color);
}
public removeApiOutlineColor(): void {
this.outlineColorStore.removeApiColor();
}
public pointerOverOutline(color: number): void {
this.outlineColorStore.pointerOver(color);
}
public pointerOutOutline(): void {
this.outlineColorStore.pointerOut();
}
public characterCloseByOutline(color: number): void {
this.outlineColorStore.characterCloseBy(color);
}
public characterFarAwayOutline(): void {
this.outlineColorStore.characterFarAway();
} }
} }
@@ -1,6 +1,7 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin; import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import type { CharacterTexture } from "../../Connexion/LocalUser"; import type { CharacterTexture } from "../../Connexion/LocalUser";
import { BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES } from "./PlayerTextures"; import { BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES } from "./PlayerTextures";
import CancelablePromise from "cancelable-promise";
export interface FrameConfig { export interface FrameConfig {
frameWidth: number; frameWidth: number;
@@ -30,7 +31,7 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
export const loadCustomTexture = ( export const loadCustomTexture = (
loaderPlugin: LoaderPlugin, loaderPlugin: LoaderPlugin,
texture: CharacterTexture texture: CharacterTexture
): Promise<BodyResourceDescriptionInterface> => { ): CancelablePromise<BodyResourceDescriptionInterface> => {
const name = "customCharacterTexture" + texture.id; const name = "customCharacterTexture" + texture.id;
const playerResourceDescriptor: BodyResourceDescriptionInterface = { name, img: texture.url, level: texture.level }; const playerResourceDescriptor: BodyResourceDescriptionInterface = { name, img: texture.url, level: texture.level };
return createLoadingPromise(loaderPlugin, playerResourceDescriptor, { return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
@@ -42,8 +43,8 @@ export const loadCustomTexture = (
export const lazyLoadPlayerCharacterTextures = ( export const lazyLoadPlayerCharacterTextures = (
loadPlugin: LoaderPlugin, loadPlugin: LoaderPlugin,
texturekeys: Array<string | BodyResourceDescriptionInterface> texturekeys: Array<string | BodyResourceDescriptionInterface>
): Promise<string[]> => { ): CancelablePromise<string[]> => {
const promisesList: Promise<unknown>[] = []; const promisesList: CancelablePromise<unknown>[] = [];
texturekeys.forEach((textureKey: string | BodyResourceDescriptionInterface) => { texturekeys.forEach((textureKey: string | BodyResourceDescriptionInterface) => {
try { try {
//TODO refactor //TODO refactor
@@ -60,12 +61,12 @@ export const lazyLoadPlayerCharacterTextures = (
console.error(err); console.error(err);
} }
}); });
let returnPromise: Promise<Array<string | BodyResourceDescriptionInterface>>; let returnPromise: CancelablePromise<Array<string | BodyResourceDescriptionInterface>>;
if (promisesList.length > 0) { if (promisesList.length > 0) {
loadPlugin.start(); loadPlugin.start();
returnPromise = Promise.all(promisesList).then(() => texturekeys); returnPromise = CancelablePromise.all(promisesList).then(() => texturekeys);
} else { } else {
returnPromise = Promise.resolve(texturekeys); returnPromise = CancelablePromise.resolve(texturekeys);
} }
//If the loading fail, we render the default model instead. //If the loading fail, we render the default model instead.
@@ -98,10 +99,17 @@ export const createLoadingPromise = (
playerResourceDescriptor: BodyResourceDescriptionInterface, playerResourceDescriptor: BodyResourceDescriptionInterface,
frameConfig: FrameConfig frameConfig: FrameConfig
) => { ) => {
return new Promise<BodyResourceDescriptionInterface>((res, rej) => { return new CancelablePromise<BodyResourceDescriptionInterface>((res, rej, cancel) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor); return res(playerResourceDescriptor);
} }
cancel(() => {
loadPlugin.off("loaderror");
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name);
return;
});
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig); loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
const errorCallback = (file: { src: string }) => { const errorCallback = (file: { src: string }) => {
if (file.src !== playerResourceDescriptor.img) return; if (file.src !== playerResourceDescriptor.img) return;
+88 -25
View File
@@ -1,15 +1,24 @@
import { requestVisitCardsStore } from "../../Stores/GameStore";
import { ActionsMenuData, actionsMenuStore } from "../../Stores/ActionsMenuStore";
import { Character } from "../Entity/Character";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type { PointInterface } from "../../Connexion/ConnexionModels"; import type { PointInterface } from "../../Connexion/ConnexionModels";
import { Character } from "../Entity/Character";
import type { PlayerAnimationDirections } from "../Player/Animation"; import type { PlayerAnimationDirections } from "../Player/Animation";
import { requestVisitCardsStore } from "../../Stores/GameStore"; import type { Unsubscriber } from "svelte/store";
import type { ActivatableInterface } from "../Game/ActivatableInterface";
import type CancelablePromise from "cancelable-promise";
/** /**
* Class representing the sprite of a remote player (a player that plays on another computer) * Class representing the sprite of a remote player (a player that plays on another computer)
*/ */
export class RemotePlayer extends Character { export class RemotePlayer extends Character implements ActivatableInterface {
userId: number; public userId: number;
public readonly activationRadius: number;
private registeredActions: { actionName: string; callback: Function }[];
private visitCardUrl: string | null; private visitCardUrl: string | null;
private isActionsMenuInitialized: boolean = false;
private actionsMenuStoreUnsubscriber: Unsubscriber;
constructor( constructor(
userId: number, userId: number,
@@ -17,39 +26,31 @@ export class RemotePlayer extends Character {
x: number, x: number,
y: number, y: number,
name: string, name: string,
texturesPromise: Promise<string[]>, texturesPromise: CancelablePromise<string[]>,
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
visitCardUrl: string | null, visitCardUrl: string | null,
companion: string | null, companion: string | null,
companionTexturePromise?: Promise<string> companionTexturePromise?: CancelablePromise<string>,
activationRadius?: number
) { ) {
super( super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
Scene,
x,
y,
texturesPromise,
name,
direction,
moving,
1,
!!visitCardUrl,
companion,
companionTexturePromise
);
//set data //set data
this.userId = userId; this.userId = userId;
this.visitCardUrl = visitCardUrl; this.visitCardUrl = visitCardUrl;
this.registeredActions = [];
this.on("pointerdown", (event: Phaser.Input.Pointer) => { this.registerDefaultActionsMenuActions();
if (event.downElement.nodeName === "CANVAS") { this.setClickable(this.registeredActions.length > 0);
requestVisitCardsStore.set(this.visitCardUrl); this.activationRadius = activationRadius ?? 96;
} this.actionsMenuStoreUnsubscriber = actionsMenuStore.subscribe((value: ActionsMenuData | undefined) => {
this.isActionsMenuInitialized = value ? true : false;
}); });
this.bindEventHandlers();
} }
updatePosition(position: PointInterface): void { public updatePosition(position: PointInterface): void {
this.playAnimation(position.direction as PlayerAnimationDirections, position.moving); this.playAnimation(position.direction as PlayerAnimationDirections, position.moving);
this.setX(position.x); this.setX(position.x);
this.setY(position.y); this.setY(position.y);
@@ -60,4 +61,66 @@ export class RemotePlayer extends Character {
this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections); this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections);
} }
} }
public registerActionsMenuAction(action: { actionName: string; callback: Function }): void {
this.registeredActions.push(action);
this.updateIsClickable();
}
public unregisterActionsMenuAction(actionName: string) {
const index = this.registeredActions.findIndex((action) => action.actionName === actionName);
if (index !== -1) {
this.registeredActions.splice(index, 1);
}
this.updateIsClickable();
}
public activate(): void {
this.toggleActionsMenu();
}
public destroy(): void {
this.actionsMenuStoreUnsubscriber();
actionsMenuStore.clear();
super.destroy();
}
public isActivatable(): boolean {
return this.isClickable();
}
private updateIsClickable(): void {
this.setClickable(this.registeredActions.length > 0);
}
private toggleActionsMenu(): void {
if (this.isActionsMenuInitialized) {
actionsMenuStore.clear();
return;
}
actionsMenuStore.initialize(this.playerName);
for (const action of this.registeredActions) {
actionsMenuStore.addAction(action.actionName, action.callback);
}
}
private registerDefaultActionsMenuActions(): void {
if (this.visitCardUrl) {
this.registeredActions.push({
actionName: "Visiting Card",
callback: () => {
requestVisitCardsStore.set(this.visitCardUrl);
actionsMenuStore.clear();
},
});
}
}
private bindEventHandlers(): void {
this.on(Phaser.Input.Events.POINTER_DOWN, (event: Phaser.Input.Pointer) => {
if (event.downElement.nodeName === "CANVAS" && event.leftButtonDown()) {
this.toggleActionsMenu();
}
});
}
} }
@@ -0,0 +1,6 @@
export interface ActivatableInterface {
readonly activationRadius: number;
isActivatable: () => boolean;
activate: () => void;
getPosition: () => { x: number; y: number };
}
@@ -0,0 +1,123 @@
import { isOutlineable } from "../../Utils/CustomTypeGuards";
import { MathUtils } from "../../Utils/MathUtils";
import type { Player } from "../Player/Player";
import type { ActivatableInterface } from "./ActivatableInterface";
export class ActivatablesManager {
// The item that can be selected by pressing the space key.
private selectedActivatableObjectByDistance?: ActivatableInterface;
private selectedActivatableObjectByPointer?: ActivatableInterface;
private activatableObjectsDistances: Map<ActivatableInterface, number> = new Map<ActivatableInterface, number>();
private currentPlayer: Player;
private canSelectByDistance: boolean = true;
private readonly outlineColor = 0xffff00;
private readonly directionalActivationPositionShift = 50;
constructor(currentPlayer: Player) {
this.currentPlayer = currentPlayer;
}
public handlePointerOverActivatableObject(object: ActivatableInterface): void {
if (this.selectedActivatableObjectByPointer === object) {
return;
}
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
}
if (isOutlineable(this.selectedActivatableObjectByPointer)) {
this.selectedActivatableObjectByPointer?.pointerOutOutline();
}
this.selectedActivatableObjectByPointer = object;
if (isOutlineable(this.selectedActivatableObjectByPointer)) {
this.selectedActivatableObjectByPointer?.pointerOverOutline(this.outlineColor);
}
}
public handlePointerOutActivatableObject(): void {
if (isOutlineable(this.selectedActivatableObjectByPointer)) {
this.selectedActivatableObjectByPointer?.pointerOutOutline();
}
this.selectedActivatableObjectByPointer = undefined;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterCloseByOutline(this.outlineColor);
}
}
public getSelectedActivatableObject(): ActivatableInterface | undefined {
return this.selectedActivatableObjectByPointer ?? this.selectedActivatableObjectByDistance;
}
public deduceSelectedActivatableObjectByDistance(): void {
if (!this.canSelectByDistance) {
return;
}
const newNearestObject = this.findNearestActivatableObject();
if (this.selectedActivatableObjectByDistance === newNearestObject) {
return;
}
// update value but do not change the outline
if (this.selectedActivatableObjectByPointer) {
this.selectedActivatableObjectByDistance = newNearestObject;
return;
}
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
}
this.selectedActivatableObjectByDistance = newNearestObject;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterCloseByOutline(this.outlineColor);
}
}
public updateActivatableObjectsDistances(objects: ActivatableInterface[]): void {
const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition(
this.directionalActivationPositionShift
);
for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance);
}
}
public updateDistanceForSingleActivatableObject(object: ActivatableInterface): void {
this.activatableObjectsDistances.set(
object,
MathUtils.distanceBetween(
this.currentPlayer.getDirectionalActivationPosition(this.directionalActivationPositionShift),
object.getPosition()
)
);
}
public disableSelectingByDistance(): void {
this.canSelectByDistance = false;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
}
this.selectedActivatableObjectByDistance = undefined;
}
public enableSelectingByDistance(): void {
this.canSelectByDistance = true;
}
private findNearestActivatableObject(): ActivatableInterface | undefined {
let shortestDistance: number = Infinity;
let closestObject: ActivatableInterface | undefined = undefined;
for (const [object, distance] of this.activatableObjectsDistances.entries()) {
if (object.isActivatable() && object.activationRadius > distance && shortestDistance > distance) {
shortestDistance = distance;
closestObject = object;
}
}
return closestObject;
}
public isSelectingByDistanceEnabled(): boolean {
return this.canSelectByDistance;
}
}
-1
View File
@@ -26,7 +26,6 @@ export class Game extends Phaser.Game {
} }
} }
}); });
} }
public step(time: number, delta: number) { public step(time: number, delta: number) {
+9 -1
View File
@@ -85,6 +85,7 @@ export class GameMap {
phaserMap phaserMap
.createLayer(layer.name, terrains, (layer.x || 0) * 32, (layer.y || 0) * 32) .createLayer(layer.name, terrains, (layer.x || 0) * 32, (layer.y || 0) * 32)
.setDepth(depth) .setDepth(depth)
.setScrollFactor(layer.parallaxx ?? 1, layer.parallaxy ?? 1)
.setAlpha(layer.opacity) .setAlpha(layer.opacity)
.setVisible(layer.visible) .setVisible(layer.visible)
.setSize(layer.width, layer.height) .setSize(layer.width, layer.height)
@@ -120,7 +121,7 @@ export class GameMap {
return []; return [];
} }
public getCollisionsGrid(): number[][] { public getCollisionGrid(): number[][] {
const grid: number[][] = []; const grid: number[][] = [];
for (let y = 0; y < this.map.height; y += 1) { for (let y = 0; y < this.map.height; y += 1) {
const row: number[] = []; const row: number[] = [];
@@ -335,12 +336,19 @@ export class GameMap {
throw new Error("No possible position found"); throw new Error("No possible position found");
} }
public getObjectWithName(name: string): ITiledMapObject | undefined {
return this.tiledObjects.find((object) => object.name === name);
}
private getLayersByKey(key: number): Array<ITiledMapLayer> { private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0); return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
} }
private isCollidingAt(x: number, y: number): boolean { private isCollidingAt(x: number, y: number): boolean {
for (const layer of this.phaserLayers) { for (const layer of this.phaserLayers) {
if (!layer.visible) {
continue;
}
if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) { if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) {
return true; return true;
} }
+196 -103
View File
@@ -1,38 +1,36 @@
import type { GameScene } from "./GameScene"; import type { GameScene } from "./GameScene";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import { scriptUtils } from "../../Api/ScriptUtils"; import { scriptUtils } from "../../Api/ScriptUtils";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager, CoWebsiteState } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON, ON_ACTION_TRIGGER_DISABLE } from "../../WebRtc/LayoutManager"; import { ON_ACTION_TRIGGER_BUTTON, ON_ICON_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap"; import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { JITSI_PRIVATE_MODE, JITSI_URL } from "../../Enum/EnvironmentVariable";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs"; import { Room } from "../../Connexion/Room";
import { LL } from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
enum OpenCoWebsiteState {
LOADING,
OPENED,
MUST_BE_CLOSE,
TRIGGER,
}
interface OpenCoWebsite { interface OpenCoWebsite {
coWebsite: CoWebsite | undefined; actionId: string;
state: OpenCoWebsiteState; coWebsite?: CoWebsite;
} }
export class GameMapPropertiesListener { export class GameMapPropertiesListener {
private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>(); private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>();
private coWebsitesIframeListeners = new Map<ITiledMapLayer, Subscription>();
private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>(); private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>();
constructor(private scene: GameScene, private gameMap: GameMap) {} constructor(private scene: GameScene, private gameMap: GameMap) {}
register() { register() {
// Website on new tab
this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
layoutManagerActionStore.removeAction("openTab"); layoutManagerActionStore.removeAction("openTab");
@@ -43,7 +41,7 @@ export class GameMapPropertiesListener {
if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE); let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE);
if (message === undefined) { if (message === undefined) {
message = get(LL).message.openWebsiteTabTrigger(); message = get(LL).trigger.newTab();
} }
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: "openTab", uuid: "openTab",
@@ -58,6 +56,129 @@ export class GameMapPropertiesListener {
} }
}); });
// Jitsi room
this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
coWebsiteManager.getCoWebsites().forEach((coWebsite) => {
if (coWebsite instanceof JitsiCoWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
});
} else {
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.scene.instance);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.scene.connection?.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
let domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") {
domain = `${location.protocol}//${domain}`;
}
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.scene.initialiseJitsi(coWebsite, roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = get(LL).trigger.jitsiRoom();
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openJitsiRoomFunction();
}
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => {
if (newValue) {
this.scene
.onMapExit(
Room.getRoomPathFromExitSceneUrl(
newValue as string,
window.location.toString(),
this.scene.MapUrlFile
)
)
.catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) {
this.scene
.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()))
.catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.scene.connection?.setSilent(false);
this.scene.CurrentPlayer.noSilent();
} else {
this.scene.connection?.setSilent(true);
this.scene.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => {
const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined;
const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined;
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => {
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: Legacy functionnality replace by layer change
this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
// Open a new co-website by the property. // Open a new co-website by the property.
this.gameMap.onEnterLayer((newLayers) => { this.gameMap.onEnterLayer((newLayers) => {
const handler = () => { const handler = () => {
@@ -104,92 +225,75 @@ export class GameMapPropertiesListener {
return; return;
} }
const actionUuid = "openWebsite-" + (Math.random() + 1).toString(36).substring(7); const actionId = "openWebsite-" + (Math.random() + 1).toString(36).substring(7);
if (this.coWebsitesOpenByLayer.has(layer)) { if (this.coWebsitesOpenByLayer.has(layer)) {
return; return;
} }
this.coWebsitesOpenByLayer.set(layer, { const coWebsiteOpen: OpenCoWebsite = {
coWebsite: undefined, actionId: actionId,
state: OpenCoWebsiteState.LOADING,
});
const openWebsiteFunction = () => {
coWebsiteManager
.loadCoWebsite(
openWebsiteProperty as string,
this.scene.MapUrlFile,
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
websitePositionProperty
)
.then((coWebsite) => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e));
this.coWebsitesOpenByLayer.delete(layer);
this.coWebsitesActionTriggerByLayer.delete(layer);
} else {
this.coWebsitesOpenByLayer.set(layer, {
coWebsite,
state: OpenCoWebsiteState.OPENED,
});
}
})
.catch((e) => console.error(e));
layoutManagerActionStore.removeAction(actionUuid);
}; };
const createWebsiteTrigger = () => { this.coWebsitesOpenByLayer.set(layer, coWebsiteOpen);
const loadCoWebsiteFunction = (coWebsite: CoWebsite) => {
coWebsiteManager.loadCoWebsite(coWebsite).catch(() => {
console.error("Error during loading a co-website: " + coWebsite.getUrl());
});
layoutManagerActionStore.removeAction(actionId);
};
const openCoWebsiteFunction = () => {
const coWebsite = new SimpleCoWebsite(
new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile),
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
false
);
coWebsiteOpen.coWebsite = coWebsite;
coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty);
loadCoWebsiteFunction(coWebsite);
};
if (
localUserStore.getForceCowebsiteTrigger() ||
websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON
) {
if (!websiteTriggerMessageProperty) { if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = get(LL).message.openWebsiteTrigger(); websiteTriggerMessageProperty = get(LL).trigger.cowebsite();
} }
this.coWebsitesOpenByLayer.set(layer, { this.coWebsitesActionTriggerByLayer.set(layer, actionId);
coWebsite: undefined,
state: OpenCoWebsiteState.TRIGGER,
});
this.coWebsitesActionTriggerByLayer.set(layer, actionUuid);
layoutManagerActionStore.addAction({ layoutManagerActionStore.addAction({
uuid: actionUuid, uuid: actionId,
type: "message", type: "message",
message: websiteTriggerMessageProperty, message: websiteTriggerMessageProperty,
callback: () => openWebsiteFunction(), callback: () => openCoWebsiteFunction(),
userInputManager: this.scene.userInputManager, userInputManager: this.scene.userInputManager,
}); });
}; } else if (websiteTriggerProperty === ON_ICON_TRIGGER_BUTTON) {
const coWebsite = new SimpleCoWebsite(
new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile),
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
false
);
this.coWebsitesIframeListeners.set( coWebsiteOpen.coWebsite = coWebsite;
layer,
iframeListener.unregisterIFrameStream.subscribe(() => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (
coWebsiteOpen?.coWebsite?.state == CoWebsiteState.CLOSED &&
(!websiteTriggerProperty || websiteTriggerProperty !== ON_ACTION_TRIGGER_DISABLE)
) {
createWebsiteTrigger();
}
})
);
const forceTrigger = localUserStore.getForceCowebsiteTrigger(); coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty);
if ( }
forceTrigger ||
(websiteTriggerProperty && websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON)
) {
createWebsiteTrigger();
} else {
this.coWebsitesOpenByLayer.set(layer, {
coWebsite: undefined,
state: OpenCoWebsiteState.LOADING,
});
openWebsiteFunction(); if (!websiteTriggerProperty) {
openCoWebsiteFunction();
} }
}); });
}; };
@@ -223,37 +327,24 @@ export class GameMapPropertiesListener {
return; return;
} }
const coWebsiteIframeListener = this.coWebsitesIframeListeners.get(layer);
if (coWebsiteIframeListener) {
coWebsiteIframeListener.unsubscribe();
this.coWebsitesIframeListeners.delete(layer);
}
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (!coWebsiteOpen) { if (!coWebsiteOpen) {
return; return;
} }
if (coWebsiteOpen.state === OpenCoWebsiteState.LOADING) { const coWebsite = coWebsiteOpen.coWebsite;
coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE;
return;
}
if ( if (coWebsite) {
coWebsiteOpen.state !== OpenCoWebsiteState.OPENED && coWebsiteManager.closeCoWebsite(coWebsite);
coWebsiteOpen.state !== OpenCoWebsiteState.TRIGGER
) {
return;
}
if (coWebsiteOpen.coWebsite !== undefined) {
coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e));
} }
this.coWebsitesOpenByLayer.delete(layer); this.coWebsitesOpenByLayer.delete(layer);
if (!websiteTriggerProperty) {
return;
}
const actionStore = get(layoutManagerActionStore); const actionStore = get(layoutManagerActionStore);
const actionTriggerUuid = this.coWebsitesActionTriggerByLayer.get(layer); const actionTriggerUuid = this.coWebsitesActionTriggerByLayer.get(layer);
@@ -269,6 +360,8 @@ export class GameMapPropertiesListener {
if (action) { if (action) {
layoutManagerActionStore.removeAction(actionTriggerUuid); layoutManagerActionStore.removeAction(actionTriggerUuid);
} }
this.coWebsitesActionTriggerByLayer.delete(layer);
}); });
}; };
+176 -287
View File
@@ -5,14 +5,14 @@ import { get, Unsubscriber } from "svelte/store";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { urlManager } from "../../Url/UrlManager"; import { urlManager } from "../../Url/UrlManager";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { UserInputManager } from "../UserInput/UserInputManager"; import { UserInputManager } from "../UserInput/UserInputManager";
import { gameManager } from "./GameManager"; import { gameManager } from "./GameManager";
import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { waScaleManager, WaScaleManagerEvent } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager"; import { EmoteManager } from "./EmoteManager";
import { soundManager } from "./SoundManager"; import { soundManager } from "./SoundManager";
import { SharedVariablesManager } from "./SharedVariablesManager"; import { SharedVariablesManager } from "./SharedVariablesManager";
@@ -20,9 +20,8 @@ import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { Room } from "../../Connexion/Room"; import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { jitsiFactory } from "../../WebRtc/JitsiFactory";
@@ -49,6 +48,7 @@ import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient"; import { analyticsClient } from "../../Administration/AnalyticsClient";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import { PathfindingManager } from "../../Utils/PathfindingManager"; import { PathfindingManager } from "../../Utils/PathfindingManager";
import { ActivatablesManager } from "./ActivatablesManager";
import type { import type {
GroupCreatedUpdatedMessageInterface, GroupCreatedUpdatedMessageInterface,
MessageUserMovedInterface, MessageUserMovedInterface,
@@ -75,7 +75,7 @@ import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { userIsAdminStore } from "../../Stores/GameStore"; import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore"; import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; import { audioManagerFileStore } from "../../Stores/AudioManagerStore";
import EVENT_TYPE = Phaser.Scenes.Events; import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
@@ -89,11 +89,14 @@ import { deepCopy } from "deep-copy-ts";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore"; import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore"; import { followUsersColorStore } from "../../Stores/FollowStore";
import Camera = Phaser.Cameras.Scene2D.Camera;
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale, LL } from "../../i18n/i18n-svelte"; import { locale } from "../../i18n/i18n-svelte";
import { i18nJson } from "../../i18n/locales"; import { i18nJson } from "../../i18n/locales";
import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
reconnecting: boolean; reconnecting: boolean;
@@ -190,8 +193,6 @@ export class GameScene extends DirtyScene {
private gameMap!: GameMap; private gameMap!: GameMap;
private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>(); private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>();
// The item that can be selected by pressing the space key.
private outlinedItem: ActionableItem | null = null;
public userInputManager!: UserInputManager; public userInputManager!: UserInputManager;
private isReconnecting: boolean | undefined = undefined; private isReconnecting: boolean | undefined = undefined;
private playerName!: string; private playerName!: string;
@@ -205,6 +206,7 @@ export class GameScene extends DirtyScene {
private emoteManager!: EmoteManager; private emoteManager!: EmoteManager;
private cameraManager!: CameraManager; private cameraManager!: CameraManager;
private pathfindingManager!: PathfindingManager; private pathfindingManager!: PathfindingManager;
private activatablesManager!: ActivatablesManager;
private preloading: boolean = true; private preloading: boolean = true;
private startPositionCalculator!: StartPositionCalculator; private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager; private sharedVariablesManager!: SharedVariablesManager;
@@ -255,7 +257,7 @@ export class GameScene extends DirtyScene {
} }
this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3"); this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3");
this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3"); this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3");
//this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); this.load.audio("audio-report-message", "/resources/objects/report-message.mp3");
this.sound.pauseOnBlur = false; this.sound.pauseOnBlur = false;
this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => {
@@ -561,6 +563,8 @@ export class GameScene extends DirtyScene {
urlManager.getStartLayerNameFromUrl() urlManager.getStartLayerNameFromUrl()
); );
startLayerNamesStore.set(this.startPositionCalculator.getStartPositionNames());
//add entities //add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>(); this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@@ -580,7 +584,7 @@ export class GameScene extends DirtyScene {
this.pathfindingManager = new PathfindingManager( this.pathfindingManager = new PathfindingManager(
this, this,
this.gameMap.getCollisionsGrid(), this.gameMap.getCollisionGrid(),
this.gameMap.getTileDimensions() this.gameMap.getTileDimensions()
); );
@@ -588,12 +592,22 @@ export class GameScene extends DirtyScene {
this.createCurrentPlayer(); this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
this.tryMovePlayerWithMoveToParameter();
this.cameraManager = new CameraManager( this.cameraManager = new CameraManager(
this, this,
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels }, { x: this.Map.widthInPixels, y: this.Map.heightInPixels },
waScaleManager waScaleManager
); );
this.pathfindingManager = new PathfindingManager(
this,
this.gameMap.getCollisionGrid(),
this.gameMap.getTileDimensions()
);
this.activatablesManager = new ActivatablesManager(this.CurrentPlayer);
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
this.cameraManager.startFollowPlayer(this.CurrentPlayer); this.cameraManager.startFollowPlayer(this.CurrentPlayer);
@@ -639,7 +653,6 @@ export class GameScene extends DirtyScene {
); );
new GameMapPropertiesListener(this, this.gameMap).register(); new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) { if (!this.room.isDisconnected()) {
this.scene.sleep(); this.scene.sleep();
@@ -650,13 +663,9 @@ export class GameScene extends DirtyScene {
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
const newPeerNumber = peers.size; const newPeerNumber = peers.size;
if (newPeerNumber > oldPeerNumber) { if (newPeerNumber > oldPeerNumber) {
this.sound.play("audio-webrtc-in", { this.playSound("audio-webrtc-in");
volume: 0.2,
});
} else if (newPeerNumber < oldPeerNumber) { } else if (newPeerNumber < oldPeerNumber) {
this.sound.play("audio-webrtc-out", { this.playSound("audio-webrtc-out");
volume: 0.2,
});
} }
oldPeerNumber = newPeerNumber; oldPeerNumber = newPeerNumber;
}); });
@@ -679,10 +688,10 @@ export class GameScene extends DirtyScene {
this.followUsersColorStoreUnsubscribe = followUsersColorStore.subscribe((color) => { this.followUsersColorStoreUnsubscribe = followUsersColorStore.subscribe((color) => {
if (color !== undefined) { if (color !== undefined) {
this.CurrentPlayer.setOutlineColor(color); this.CurrentPlayer.setFollowOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color); this.connection?.emitPlayerOutlineColor(color);
} else { } else {
this.CurrentPlayer.removeOutlineColor(); this.CurrentPlayer.removeFollowOutlineColor();
this.connection?.emitPlayerOutlineColor(null); this.connection?.emitPlayerOutlineColor(null);
} }
}); });
@@ -699,10 +708,6 @@ export class GameScene extends DirtyScene {
); );
} }
public activateOutlinedItem(): void {
this.outlinedItem?.activate();
}
/** /**
* Initializes the connection to Pusher. * Initializes the connection to Pusher.
*/ */
@@ -814,7 +819,19 @@ export class GameScene extends DirtyScene {
* Triggered when we receive the JWT token to connect to Jitsi * Triggered when we receive the JWT token to connect to Jitsi
*/ */
this.connection.sendJitsiJwtMessageStream.subscribe((message) => { this.connection.sendJitsiJwtMessageStream.subscribe((message) => {
this.startJitsi(message.jitsiRoom, message.jwt); if (!JITSI_URL) {
throw new Error("Missing JITSI_URL environment variable.");
}
let domain = JITSI_URL;
if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") {
domain = `${location.protocol}//${domain}`;
}
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.initialiseJitsi(coWebsite, message.jitsiRoom, message.jwt);
}); });
this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => { this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => {
@@ -825,11 +842,8 @@ export class GameScene extends DirtyScene {
this.simplePeer = new SimplePeer(this.connection); this.simplePeer = new SimplePeer(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
//listen event to share position of user
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this));
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this));
this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => {
this.gameMap.setPosition(event.x, event.y); this.handleCurrentPlayerHasMovedEvent(event);
}); });
// Set up variables manager // Set up variables manager
@@ -954,103 +968,6 @@ export class GameScene extends DirtyScene {
} }
} }
private triggerOnMapLayerPropertyChange() {
this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => {
if (newValue) {
this.onMapExit(
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
).catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) {
this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) =>
console.error(e)
);
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
this.stopJitsi();
} else {
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
this.startJitsi(roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = get(LL).message.openJitsiTrigger();
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.userInputManager,
});
} else {
openJitsiRoomFunction();
}
}
});
this.gameMap.onPropertyChange(GameMapProperties.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(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => {
const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined;
const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined;
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => {
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: Legacy functionnality replace by layer change
this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
}
private listenToIframeEvents(): void { private listenToIframeEvents(): void {
this.iframeSubscriptionList = []; this.iframeSubscriptionList = [];
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
@@ -1296,21 +1213,20 @@ export class GameScene extends DirtyScene {
throw new Error("Unknown query source"); throw new Error("Unknown query source");
} }
const coWebsite = await coWebsiteManager.loadCoWebsite( const coWebsite: SimpleCoWebsite = new SimpleCoWebsite(
openCoWebsite.url, new URL(openCoWebsite.url, iframeListener.getBaseUrlFromSource(source)),
iframeListener.getBaseUrlFromSource(source),
openCoWebsite.allowApi, openCoWebsite.allowApi,
openCoWebsite.allowPolicy, openCoWebsite.allowPolicy,
openCoWebsite.position openCoWebsite.widthPercent,
openCoWebsite.closable ?? true
); );
if (!coWebsite) { if (openCoWebsite.lazy === undefined || !openCoWebsite.lazy) {
throw new Error("Error on opening co-website"); await coWebsiteManager.loadCoWebsite(coWebsite);
} }
return { return {
id: coWebsite.iframe.id, id: coWebsite.getId(),
position: coWebsite.position,
}; };
}); });
@@ -1319,28 +1235,23 @@ export class GameScene extends DirtyScene {
return coWebsites.map((coWebsite: CoWebsite) => { return coWebsites.map((coWebsite: CoWebsite) => {
return { return {
id: coWebsite.iframe.id, id: coWebsite.getId(),
position: coWebsite.position,
}; };
}); });
}); });
iframeListener.registerAnswerer("closeCoWebsite", async (coWebsiteId) => { iframeListener.registerAnswerer("closeCoWebsite", (coWebsiteId) => {
const coWebsite = coWebsiteManager.getCoWebsiteById(coWebsiteId); const coWebsite = coWebsiteManager.getCoWebsiteById(coWebsiteId);
if (!coWebsite) { if (!coWebsite) {
throw new Error("Unknown co-website"); throw new Error("Unknown co-website");
} }
return coWebsiteManager.closeCoWebsite(coWebsite).catch((error) => { return coWebsiteManager.closeCoWebsite(coWebsite);
throw new Error("Error on closing co-website");
});
}); });
iframeListener.registerAnswerer("closeCoWebsites", async () => { iframeListener.registerAnswerer("closeCoWebsites", () => {
return await coWebsiteManager.closeCoWebsites().catch((error) => { return coWebsiteManager.closeCoWebsites();
throw new Error("Error on closing all co-websites");
});
}); });
iframeListener.registerAnswerer("getProperty", (data) => { iframeListener.registerAnswerer("getProperty", (data) => {
@@ -1439,7 +1350,7 @@ export class GameScene extends DirtyScene {
//Create new colliders with the new GameMap //Create new colliders with the new GameMap
this.createCollisionWithPlayer(); this.createCollisionWithPlayer();
//Create new trigger with the new GameMap //Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange(); new GameMapPropertiesListener(this, this.gameMap).register();
resolve(newFirstgid); resolve(newFirstgid);
}); });
}); });
@@ -1493,12 +1404,12 @@ export class GameScene extends DirtyScene {
const green = normalizeColor(message.green); const green = normalizeColor(message.green);
const blue = normalizeColor(message.blue); const blue = normalizeColor(message.blue);
const color = (red << 16) | (green << 8) | blue; const color = (red << 16) | (green << 8) | blue;
this.CurrentPlayer.setOutlineColor(color); this.CurrentPlayer.setApiOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color); this.connection?.emitPlayerOutlineColor(color);
}); });
iframeListener.registerAnswerer("removePlayerOutline", (message) => { iframeListener.registerAnswerer("removePlayerOutline", (message) => {
this.CurrentPlayer.removeOutlineColor(); this.CurrentPlayer.removeApiOutlineColor();
this.connection?.emitPlayerOutlineColor(null); this.connection?.emitPlayerOutlineColor(null);
}); });
@@ -1510,9 +1421,9 @@ export class GameScene extends DirtyScene {
}); });
iframeListener.registerAnswerer("movePlayerTo", async (message) => { iframeListener.registerAnswerer("movePlayerTo", async (message) => {
const index = this.getGameMap().getTileIndexAt(message.x, message.y); const startTileIndex = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const startTile = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y); const destinationTileIndex = this.getGameMap().getTileIndexAt(message.x, message.y);
const path = await this.getPathfindingManager().findPath(startTile, index, true, true); const path = await this.getPathfindingManager().findPath(startTileIndex, destinationTileIndex, true, true);
path.shift(); path.shift();
if (path.length === 0) { if (path.length === 0) {
throw new Error("no path available"); throw new Error("no path available");
@@ -1552,14 +1463,15 @@ export class GameScene extends DirtyScene {
phaserLayers[i].setCollisionByProperty({ collides: true }, visible); phaserLayers[i].setCollisionByProperty({ collides: true }, visible);
} }
} }
this.pathfindingManager.setCollisionGrid(this.gameMap.getCollisionGrid());
this.markDirty(); this.markDirty();
} }
private getMapDirUrl(): string { public getMapDirUrl(): string {
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); return this.MapUrlFile.substring(0, this.MapUrlFile.lastIndexOf("/"));
} }
private async onMapExit(roomUrl: URL) { public async onMapExit(roomUrl: URL) {
if (this.mapTransitioning) return; if (this.mapTransitioning) return;
this.mapTransitioning = true; this.mapTransitioning = true;
@@ -1610,16 +1522,21 @@ export class GameScene extends DirtyScene {
} }
} }
public playSound(sound: string) {
this.sound.play(sound, {
volume: 0.2,
});
}
public cleanupClosingScene(): void { public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi // stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsites().catch((e) => console.error(e)); coWebsiteManager.closeCoWebsites();
// Stop the script, if any // Stop the script, if any
const scripts = this.getScriptUrls(this.mapFile); const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) { for (const script of scripts) {
iframeListener.unregisterScript(script); iframeListener.unregisterScript(script);
} }
this.stopJitsi();
audioManagerFileStore.unloadAudio(); audioManagerFileStore.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection?.closeConnection(); this.connection?.closeConnection();
@@ -1647,7 +1564,10 @@ export class GameScene extends DirtyScene {
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close(); this.embeddedWebsiteManager?.close();
mediaManager.hideGameOverlay(); //When we leave game, the camera is stop to be reopen after.
// I think that we could keep camera status and the scene can manage camera setup
//TODO find wy chrome don't manage correctly a multiple ask mediaDevices
//mediaManager.hideMyCamera();
for (const iframeEvents of this.iframeSubscriptionList) { for (const iframeEvents of this.iframeSubscriptionList) {
iframeEvents.unsubscribe(); iframeEvents.unsubscribe();
@@ -1667,6 +1587,36 @@ export class GameScene extends DirtyScene {
this.MapPlayersByKey.clear(); this.MapPlayersByKey.clear();
} }
private tryMovePlayerWithMoveToParameter(): void {
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
let endPos;
const posFromParam = StringUtils.parsePointFromParam(moveToParam);
if (posFromParam) {
endPos = this.gameMap.getTileIndexAt(posFromParam.x, posFromParam.y);
} else {
const destinationObject = this.gameMap.getObjectWithName(moveToParam);
if (destinationObject) {
endPos = this.gameMap.getTileIndexAt(destinationObject.x, destinationObject.y);
} else {
endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
}
}
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
}
private getExitUrl(layer: ITiledMapLayer): string | undefined { private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined; return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined;
} }
@@ -1722,7 +1672,18 @@ export class GameScene extends DirtyScene {
} }
} }
createCollisionWithPlayer() { private handleCurrentPlayerHasMovedEvent(event: HasPlayerMovedEvent): void {
//listen event to share position of user
this.pushPlayerPosition(event);
this.gameMap.setPosition(event.x, event.y);
this.activatablesManager.updateActivatableObjectsDistances([
...Array.from(this.MapPlayersByKey.values()),
...this.actionableItems.values(),
]);
this.activatablesManager.deduceSelectedActivatableObjectByDistance();
}
private createCollisionWithPlayer() {
//add collision layer //add collision layer
for (const phaserLayer of this.gameMap.phaserLayers) { for (const phaserLayer of this.gameMap.phaserLayers) {
this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => {
@@ -1741,7 +1702,7 @@ export class GameScene extends DirtyScene {
} }
} }
createCurrentPlayer() { private createCurrentPlayer() {
//TODO create animation moving between exit and start //TODO create animation moving between exit and start
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers); const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers);
try { try {
@@ -1756,7 +1717,7 @@ export class GameScene extends DirtyScene {
this.companion, this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
); );
this.CurrentPlayer.on("pointerdown", (pointer: Phaser.Input.Pointer) => { this.CurrentPlayer.on(Phaser.Input.Events.POINTER_DOWN, (pointer: Phaser.Input.Pointer) => {
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) { if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
return; //we don't want the menu to open when pinching on a touch screen. return; //we don't want the menu to open when pinching on a touch screen.
} }
@@ -1768,26 +1729,16 @@ export class GameScene extends DirtyScene {
emoteMenuStore.openEmoteMenu(); emoteMenuStore.openEmoteMenu();
} }
}); });
this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OVER, (pointer: Phaser.Input.Pointer) => {
this.CurrentPlayer.pointerOverOutline(0x00ffff);
});
this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OUT, (pointer: Phaser.Input.Pointer) => {
this.CurrentPlayer.pointerOutOutline();
});
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => { this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
this.connection?.emitEmoteEvent(emoteKey); this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey); analyticsClient.launchEmote(emoteKey);
}); });
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) { } catch (err) {
if (err instanceof TextureError) { if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
@@ -1799,7 +1750,7 @@ export class GameScene extends DirtyScene {
this.createCollisionWithPlayer(); this.createCollisionWithPlayer();
} }
pushPlayerPosition(event: HasPlayerMovedEvent) { private pushPlayerPosition(event: HasPlayerMovedEvent) {
if (this.lastMoveEventSent === event) { if (this.lastMoveEventSent === event) {
return; return;
} }
@@ -1825,49 +1776,6 @@ export class GameScene extends DirtyScene {
// Otherwise, do nothing. // Otherwise, do nothing.
} }
/**
* Finds the correct item to outline and outline it (if there is an item to be outlined)
* @param event
*/
private outlineItem(event: HasPlayerMovedEvent): void {
let x = event.x;
let y = event.y;
switch (event.direction) {
case PlayerAnimationDirections.Up:
y -= 32;
break;
case PlayerAnimationDirections.Down:
y += 32;
break;
case PlayerAnimationDirections.Left:
x -= 32;
break;
case PlayerAnimationDirections.Right:
x += 32;
break;
default:
throw new Error('Unexpected direction "' + event.direction + '"');
}
let shortestDistance: number = Infinity;
let selectedItem: ActionableItem | null = null;
for (const item of this.actionableItems.values()) {
const distance = item.actionableDistance(x, y);
if (distance !== null && distance < shortestDistance) {
shortestDistance = distance;
selectedItem = item;
}
}
if (this.outlinedItem === selectedItem) {
return;
}
this.outlinedItem?.notSelectable();
this.outlinedItem = selectedItem;
this.outlinedItem?.selectable();
}
private doPushPlayerPosition(event: HasPlayerMovedEvent): void { private doPushPlayerPosition(event: HasPlayerMovedEvent): void {
this.lastMoveEventSent = event; this.lastMoveEventSent = event;
this.lastSentTick = this.currentTick; this.lastSentTick = this.currentTick;
@@ -1885,7 +1793,7 @@ export class GameScene extends DirtyScene {
* @param time * @param time
* @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
*/ */
update(time: number, delta: number): void { public update(time: number, delta: number): void {
this.dirty = false; this.dirty = false;
this.currentTick = time; this.currentTick = time;
this.CurrentPlayer.moveUser(delta, this.userInputManager.getEventListForGameTick()); this.CurrentPlayer.moveUser(delta, this.userInputManager.getEventListForGameTick());
@@ -1904,9 +1812,15 @@ export class GameScene extends DirtyScene {
case "RemovePlayerEvent": case "RemovePlayerEvent":
this.doRemovePlayer(event.userId); this.doRemovePlayer(event.userId);
break; break;
case "UserMovedEvent": case "UserMovedEvent": {
this.doUpdatePlayerPosition(event.event); this.doUpdatePlayerPosition(event.event);
const remotePlayer = this.MapPlayersByKey.get(event.event.userId);
if (remotePlayer) {
this.activatablesManager.updateDistanceForSingleActivatableObject(remotePlayer);
this.activatablesManager.deduceSelectedActivatableObjectByDistance();
}
break; break;
}
case "GroupCreatedUpdatedEvent": case "GroupCreatedUpdatedEvent":
this.doShareGroupPosition(event.event); this.doShareGroupPosition(event.event);
break; break;
@@ -1993,11 +1907,21 @@ export class GameScene extends DirtyScene {
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
); );
if (addPlayerData.outlineColor !== undefined) { if (addPlayerData.outlineColor !== undefined) {
player.setOutlineColor(addPlayerData.outlineColor); player.setApiOutlineColor(addPlayerData.outlineColor);
} }
this.MapPlayers.add(player); this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player); this.MapPlayersByKey.set(player.userId, player);
player.updatePosition(addPlayerData.position); player.updatePosition(addPlayerData.position);
player.on(Phaser.Input.Events.POINTER_OVER, () => {
this.activatablesManager.handlePointerOverActivatableObject(player);
this.markDirty();
});
player.on(Phaser.Input.Events.POINTER_OUT, () => {
this.activatablesManager.handlePointerOutActivatableObject();
this.markDirty();
});
} }
/** /**
@@ -2027,7 +1951,7 @@ export class GameScene extends DirtyScene {
this.playersPositionInterpolator.removePlayer(userId); this.playersPositionInterpolator.removePlayer(userId);
} }
public updatePlayerPosition(message: MessageUserMovedInterface): void { private updatePlayerPosition(message: MessageUserMovedInterface): void {
this.pendingEvents.enqueue({ this.pendingEvents.enqueue({
type: "UserMovedEvent", type: "UserMovedEvent",
event: message, event: message,
@@ -2057,7 +1981,7 @@ export class GameScene extends DirtyScene {
this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement);
} }
public shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { private shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) {
this.pendingEvents.enqueue({ this.pendingEvents.enqueue({
type: "GroupCreatedUpdatedEvent", type: "GroupCreatedUpdatedEvent",
event: groupPositionMessage, event: groupPositionMessage,
@@ -2109,9 +2033,9 @@ export class GameScene extends DirtyScene {
return; return;
} }
if (message.removeOutlineColor) { if (message.removeOutlineColor) {
character.removeOutlineColor(); character.removeApiOutlineColor();
} else { } else {
character.setOutlineColor(message.outlineColor); character.setApiOutlineColor(message.outlineColor);
} }
} }
@@ -2156,7 +2080,18 @@ export class GameScene extends DirtyScene {
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
} }
public startJitsi(roomName: string, jwt?: string): void { public enableMediaBehaviors() {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT);
this.connection?.setSilent(!!silent);
mediaManager.showMyCamera();
}
public disableMediaBehaviors() {
this.connection?.setSilent(true);
mediaManager.hideMyCamera();
}
public initialiseJitsi(coWebsite: JitsiCoWebsite, roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties(); const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring( const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined, allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
@@ -2167,71 +2102,21 @@ export class GameScene extends DirtyScene {
GameMapProperties.JITSI_INTERFACE_CONFIG GameMapProperties.JITSI_INTERFACE_CONFIG
); );
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined;
const jitsiKeepCircle = allProps.get("jitsiKeepCircle") as boolean | false; coWebsite.setJitsiLoadPromise(() => {
return jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl);
jitsiFactory
.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth)
.catch((e) => console.error(e));
this.connection?.setSilent(true);
mediaManager.hideGameOverlay();
if (jitsiKeepCircle) {
const silent = this.gameMap.getCurrentProperties().get("silent");
this.connection?.setSilent(!!silent);
mediaManager.showGameOverlay();
}
analyticsClient.enteredJitsi(roomName, this.room.id);
//permit to stop jitsi when user close iframe
mediaManager.addTriggerCloseJitsiFrameButton("close-jitsi", () => {
this.stopJitsi();
}); });
}
public stopJitsi(): void { coWebsiteManager.loadCoWebsite(coWebsite).catch((err) => {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT); console.error(err);
this.connection?.setSilent(!!silent); });
jitsiFactory.stop();
mediaManager.showGameOverlay();
const allProps = this.gameMap.getCurrentProperties(); if (allProps.get("jitsiKeepCircle") as boolean | false) {
this.enableMediaBehaviors();
if (allProps.get("jitsiRoom") === undefined) {
layoutManagerActionStore.removeAction("jitsi");
} else { } else {
const openJitsiRoomFunction = () => { this.disableMediaBehaviors();
const roomName = jitsiFactory.getRoomName(
allProps.get(GameMapProperties.JITSI_ROOM) as string,
this.instance
);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.connection && this.connection.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
this.startJitsi(roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to enter Jitsi Meet room";
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.userInputManager,
});
} }
analyticsClient.enteredJitsi(roomName, this.room.id);
mediaManager.removeTriggerCloseJitsiFrameButton("close-jitsi");
} }
//todo: put this into an 'orchestrator' scene (EntryScene?) //todo: put this into an 'orchestrator' scene (EntryScene?)
@@ -2307,4 +2192,8 @@ export class GameScene extends DirtyScene {
public getPathfindingManager(): PathfindingManager { public getPathfindingManager(): PathfindingManager {
return this.pathfindingManager; return this.pathfindingManager;
} }
public getActivatablesManager(): ActivatablesManager {
return this.activatablesManager;
}
} }
@@ -0,0 +1,10 @@
export interface OutlineableInterface {
setFollowOutlineColor(color: number): void;
removeFollowOutlineColor(): void;
setApiOutlineColor(color: number): void;
removeApiOutlineColor(): void;
pointerOverOutline(color: number): void;
pointerOutOutline(): void;
characterCloseByOutline(color: number): void;
characterFarAwayOutline(): void;
}
+1 -1
View File
@@ -41,7 +41,7 @@ export class PlayerMovement {
oldX: this.startPosition.x, oldX: this.startPosition.x,
oldY: this.startPosition.y, oldY: this.startPosition.y,
direction: this.endPosition.direction, direction: this.endPosition.direction,
moving: this.endPosition.moving, moving: this.isOutdated(tick) ? false : this.endPosition.moving,
}; };
} }
} }
@@ -16,32 +16,6 @@ export class StartPositionCalculator {
) { ) {
this.initStartXAndStartY(); this.initStartXAndStartY();
} }
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
/** /**
* *
@@ -77,6 +51,47 @@ export class StartPositionCalculator {
} }
} }
public getStartPositionNames(): string[] {
const names: string[] = [];
for (const layer of this.gameMap.flatLayers) {
if (layer.name === "start") {
names.push(layer.name);
continue;
}
if (this.isStartLayer(layer)) {
names.push(layer.name);
}
}
return names;
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
private isStartLayer(layer: ITiledMapLayer): boolean { private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, GameMapProperties.START_LAYER) == true; return this.getProperty(layer, GameMapProperties.START_LAYER) == true;
} }
+11 -5
View File
@@ -5,10 +5,11 @@
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import type { ActivatableInterface } from "../Game/ActivatableInterface";
type EventCallback = (state: unknown, parameters: unknown) => void; type EventCallback = (state: unknown, parameters: unknown) => void;
export class ActionableItem { export class ActionableItem implements ActivatableInterface {
private readonly activationRadiusSquared: number; private readonly activationRadiusSquared: number;
private isSelectable: boolean = false; private isSelectable: boolean = false;
private callbacks: Map<string, Array<EventCallback>> = new Map<string, Array<EventCallback>>(); private callbacks: Map<string, Array<EventCallback>> = new Map<string, Array<EventCallback>>();
@@ -17,7 +18,7 @@ export class ActionableItem {
private id: number, private id: number,
private sprite: Sprite, private sprite: Sprite,
private eventHandler: GameScene, private eventHandler: GameScene,
private activationRadius: number, public readonly activationRadius: number,
private onActivateCallback: (item: ActionableItem) => void private onActivateCallback: (item: ActionableItem) => void
) { ) {
this.activationRadiusSquared = activationRadius * activationRadius; this.activationRadiusSquared = activationRadius * activationRadius;
@@ -40,6 +41,10 @@ export class ActionableItem {
} }
} }
public getPosition(): { x: number; y: number } {
return { x: this.sprite.x, y: this.sprite.y };
}
/** /**
* Show the outline of the sprite. * Show the outline of the sprite.
*/ */
@@ -70,9 +75,10 @@ export class ActionableItem {
return this.sprite.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; return this.sprite.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
} }
/** public isActivatable(): boolean {
* Triggered when the "space" key is pressed and the object is in range of being activated. return this.isSelectable;
*/ }
public activate(): void { public activate(): void {
this.onActivateCallback(this); this.onActivateCallback(this);
} }
@@ -3,11 +3,12 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import { loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import type { CharacterTexture } from "../../Connexion/LocalUser"; import type { CharacterTexture } from "../../Connexion/LocalUser";
import type CancelablePromise from "cancelable-promise";
export abstract class AbstractCharacterScene extends ResizableScene { export abstract class AbstractCharacterScene extends ResizableScene {
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> { loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures(); const textures = this.getTextures();
const promises: Promise<BodyResourceDescriptionInterface>[] = []; const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
if (textures) { if (textures) {
for (const texture of textures) { for (const texture of textures) {
if (texture.level === -1) { if (texture.level === -1) {
@@ -21,7 +22,7 @@ export abstract class AbstractCharacterScene extends ResizableScene {
loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> { loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures(); const textures = this.getTextures();
const promises: Promise<BodyResourceDescriptionInterface>[] = []; const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
if (textures) { if (textures) {
for (const texture of textures) { for (const texture of textures) {
if (texture.level !== -1) { if (texture.level !== -1) {
+3 -4
View File
@@ -11,10 +11,10 @@ import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import { SelectCharacterSceneName } from "./SelectCharacterScene"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore"; import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { CustomizedCharacter } from "../Entity/CustomizedCharacter"; import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { analyticsClient } from "../../Administration/AnalyticsClient"; import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
export const CustomizeSceneName = "CustomizeScene"; export const CustomizeSceneName = "CustomizeScene";
@@ -67,12 +67,12 @@ export class CustomizeScene extends AbstractCharacterScene {
customCharacterSceneVisibleStore.set(true); customCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1;
customCharacterSceneVisibleStore.set(true); customCharacterSceneVisibleStore.set(true);
}); });
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1;
this.Rectangle = this.add.rectangle( this.Rectangle = this.add.rectangle(
this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.x + this.cameras.main.width / 2,
@@ -289,7 +289,6 @@ export class CustomizeScene extends AbstractCharacterScene {
gameManager.setCharacterLayers(layers); gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName); this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom(); waScaleManager.restoreZoom();
this.events.removeListener("wake");
gameManager.tryResumingGame(EnableCameraSceneName); gameManager.tryResumingGame(EnableCameraSceneName);
customCharacterSceneVisibleStore.set(false); customCharacterSceneVisibleStore.set(false);
} }
+1 -2
View File
@@ -8,8 +8,6 @@ import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { localeDetector } from "../../i18n/locales"; import { localeDetector } from "../../i18n/locales";
const $LL = get(LL);
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
/** /**
@@ -43,6 +41,7 @@ export class EntryScene extends Scene {
this.scene.start(nextSceneName); this.scene.start(nextSceneName);
}) })
.catch((err) => { .catch((err) => {
const $LL = get(LL);
if (err.response && err.response.status == 404) { if (err.response && err.response.status == 404) {
ErrorScene.showError( ErrorScene.showError(
new WAError( new WAError(
@@ -12,8 +12,8 @@ import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { analyticsClient } from "../../Administration/AnalyticsClient"; import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
@@ -60,7 +60,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
}); });
@@ -69,7 +69,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff); this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);
@@ -9,7 +9,7 @@ import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore"; import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable"; import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
export const SelectCompanionSceneName = "SelectCompanionScene"; export const SelectCompanionSceneName = "SelectCompanionScene";
@@ -44,7 +44,7 @@ export class SelectCompanionScene extends ResizableScene {
selectCompanionSceneVisibleStore.set(true); selectCompanionSceneVisibleStore.set(true);
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
new PinchManager(this); new PinchManager(this);
+2
View File
@@ -78,6 +78,8 @@ export interface ITiledMapTileLayer {
width: number; width: number;
x: number; x: number;
y: number; y: number;
parallaxx?: number;
parallaxy?: number;
/** /**
* Draw order (topdown (default), index) * Draw order (topdown (default), index)
+3 -2
View File
@@ -6,6 +6,7 @@ import { Character } from "../Entity/Character";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { userMovingStore } from "../../Stores/GameStore"; import { userMovingStore } from "../../Stores/GameStore";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore"; import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
import type CancelablePromise from "cancelable-promise";
export const hasMovedEventName = "hasMoved"; export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote"; export const requestEmoteEventName = "requestEmote";
@@ -20,11 +21,11 @@ export class Player extends Character {
x: number, x: number,
y: number, y: number,
name: string, name: string,
texturesPromise: Promise<string[]>, texturesPromise: CancelablePromise<string[]>,
direction: PlayerAnimationDirections, direction: PlayerAnimationDirections,
moving: boolean, moving: boolean,
companion: string | null, companion: string | null,
companionTexturePromise?: Promise<string> companionTexturePromise?: CancelablePromise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise); super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
+5 -7
View File
@@ -37,19 +37,17 @@ export class WaScaleManager {
height: height * devicePixelRatio, height: height * devicePixelRatio,
}); });
if (gameSize.width == 0) { if (realSize.width !== 0 && gameSize.width !== 0 && devicePixelRatio !== 0) {
return; this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
} }
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height); this.scaleManager.resize(gameSize.width, gameSize.height);
this.scaleManager.setZoom(this.actualZoom);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves // Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style; const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + "px"; style.width = Math.ceil(realSize.width !== 0 ? realSize.width / devicePixelRatio : 0) + "px";
style.height = Math.ceil(realSize.height / devicePixelRatio) + "px"; style.height = Math.ceil(realSize.height !== 0 ? realSize.height / devicePixelRatio : 0) + "px";
// Resize the game element at the same size at the canvas // Resize the game element at the same size at the canvas
const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game").style; const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game").style;
@@ -1,3 +1,6 @@
import { Player } from "../Player/Player";
import { RemotePlayer } from "../Entity/RemotePlayer";
import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface"; import type { UserInputHandlerInterface } from "../../Interfaces/UserInputHandlerInterface";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
@@ -23,10 +26,16 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
} }
public handlePointerUpEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void { public handlePointerUpEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {
if (pointer.rightButtonReleased() || pointer.getDuration() > 250) { if ((!pointer.wasTouch && pointer.leftButtonReleased()) || pointer.getDuration() > 250) {
return; return;
} }
for (const object of gameObjects) {
if (object instanceof Player || object instanceof RemotePlayer) {
return;
}
}
if ( if (
this.lastTime > 0 && this.lastTime > 0 &&
pointer.time - this.lastTime < 500 && pointer.time - this.lastTime < 500 &&
@@ -46,7 +55,7 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
.then((path) => { .then((path) => {
// Remove first step as it is for the tile we are currently standing on // Remove first step as it is for the tile we are currently standing on
path.shift(); path.shift();
this.gameScene.CurrentPlayer.setPathToFollow(path).catch((reason) => {}); this.gameScene.CurrentPlayer.setPathToFollow(path).catch((reason) => { });
}) })
.catch((reason) => { .catch((reason) => {
console.warn(reason); console.warn(reason);
@@ -58,10 +67,23 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
this.lastY = pointer.y; this.lastY = pointer.y;
} }
public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {} public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void { }
public handleSpaceKeyUpEvent(event: Event): Event { public handleSpaceKeyUpEvent(event: Event): Event {
this.gameScene.activateOutlinedItem(); const activatableManager = this.gameScene.getActivatablesManager();
const activatable = activatableManager.getSelectedActivatableObject();
if (activatable && activatable.isActivatable() && activatableManager.isSelectingByDistanceEnabled()) {
activatable.activate();
}
return event; return event;
} }
public addSpaceEventListener(callback: Function): void {
this.gameScene.input.keyboard.addListener("keyup-SPACE", callback);
this.gameScene.getActivatablesManager().disableSelectingByDistance();
}
public removeSpaceEventListner(callback: Function): void {
this.gameScene.input.keyboard.removeListener("keyup-SPACE", callback);
this.gameScene.getActivatablesManager().enableSelectingByDistance();
}
} }
@@ -223,10 +223,10 @@ export class UserInputManager {
} }
addSpaceEventListner(callback: Function) { addSpaceEventListner(callback: Function) {
this.scene.input.keyboard.addListener("keyup-SPACE", callback); this.userInputHandler.addSpaceEventListener(callback);
} }
removeSpaceEventListner(callback: Function) { removeSpaceEventListner(callback: Function) {
this.scene.input.keyboard.removeListener("keyup-SPACE", callback); this.userInputHandler.removeSpaceEventListner(callback);
} }
destroy(): void { destroy(): void {
+43
View File
@@ -0,0 +1,43 @@
import { writable } from "svelte/store";
export interface ActionsMenuData {
playerName: string;
actions: { actionName: string; callback: Function }[];
}
function createActionsMenuStore() {
const { subscribe, update, set } = writable<ActionsMenuData | undefined>(undefined);
return {
subscribe,
initialize: (playerName: string) => {
set({
playerName,
actions: [],
});
},
addAction: (actionName: string, callback: Function) => {
update((data) => {
data?.actions.push({ actionName, callback });
return data;
});
},
removeAction: (actionName: string) => {
update((data) => {
const actionIndex = data?.actions.findIndex((action) => action.actionName === actionName);
if (actionIndex !== undefined && actionIndex != -1) {
data?.actions.splice(actionIndex, 1);
}
return data;
});
},
/**
* Hides menu
*/
clear: () => {
set(undefined);
},
};
}
export const actionsMenuStore = createActionsMenuStore();
@@ -2,14 +2,14 @@ import { get, writable } from "svelte/store";
import type { Box } from "../WebRtc/LayoutManager"; import type { Box } from "../WebRtc/LayoutManager";
import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import { LayoutMode } from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import { layoutModeStore } from "./StreamableCollectionStore"; import { embedScreenLayout } from "./EmbedScreensStore";
/** /**
* Tries to find the biggest available box of remaining space (this is a space where we can center the character) * Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/ */
function findBiggestAvailableArea(): Box { function findBiggestAvailableArea(): Box {
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas"); const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
if (get(layoutModeStore) === LayoutMode.VideoChat) { if (get(embedScreenLayout) === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div"); const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
const htmlChildren = Array.from(children.values()); const htmlChildren = Array.from(children.values());

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