merged develop

This commit is contained in:
Hanusiak Piotr 2021-12-15 15:23:21 +01:00
commit 99ffb7b450
177 changed files with 5904 additions and 2328 deletions

View file

@ -20,6 +20,15 @@ jobs:
- name: "Checkout" - name: "Checkout"
uses: "actions/checkout@v2.0.0" uses: "actions/checkout@v2.0.0"
- name: "Setup NodeJS"
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: "Install dependencies"
run: npm install
working-directory: "tests"
- name: "Setup .env file" - name: "Setup .env file"
run: cp .env.template .env run: cp .env.template .env
@ -27,10 +36,10 @@ jobs:
run: sudo chown 1000:1000 -R . run: sudo chown 1000:1000 -R .
- name: "Start environment" - name: "Start environment"
run: docker-compose up -d run: LIVE_RELOAD=0 docker-compose up -d
- name: "Wait for environment to build (and downloading testcafe image)" - name: "Wait for environment to build (and downloading testcafe image)"
run: (docker-compose -f docker-compose.testcafe.yml pull &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully" run: (docker-compose -f docker-compose.testcafe.yml build &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
# - name: "temp debug: display logs" # - name: "temp debug: display logs"
# run: docker-compose logs # run: docker-compose logs
@ -42,7 +51,7 @@ jobs:
# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port" # run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port"
- name: "Run tests" - name: "Run tests"
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe run: PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
- name: Upload failed tests - name: Upload failed tests
if: ${{ failure() }} if: ${{ failure() }}

View file

@ -10,6 +10,14 @@ We love to receive contributions from our community — you!
There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, There are many ways to contribute, from writing tutorials or blog posts, improving the documentation,
submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself.
## Contributing external resources
You can share your work on maps / articles / videos related to WorkAdventure on our [awesome-workadventure](https://github.com/workadventure/awesome-workadventure) list.
## Developer documentation
Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/)
## Using the issue tracker ## Using the issue tracker
First things first: **Do NOT report security vulnerabilities in public issues!**. First things first: **Do NOT report security vulnerabilities in public issues!**.
@ -59,9 +67,43 @@ $ docker-compose exec back yarn run pretty
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine). Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine), or an end-to-end test (we use Testcafe).
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file some description text describing how to test the feature.
to add a reference to your newly created test map.
* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference
to your newly created test map
* if the features can be automatically tested, please provide a testcafe test
#### Running testcafe tests
End-to-end tests are available in the "/tests" directory.
To run these tests locally:
```console
$ LIVE_RELOAD=0 docker-compose up -d
$ cd tests
$ npm install
$ npm run test
```
Note: If your tests fail on a Javascript error in "sockjs", this is due to the
Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting
WorkAdventure with the `LIVE_RELOAD=0` environment variable.
End-to-end tests can take a while to run. To run only one test, use:
```console
$ npm run test -- tests/[name of the test file].ts
```
You can also run the tests inside a container (but you will not have visual feedbacks on your test, so we recommend using
the local tests).
```console
$ LIVE_RELOAD=0 docker-compose up -d
# Wait 2-3 minutes for the environment to start, then:
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
```

View file

@ -14,6 +14,10 @@ In WorkAdventure you can move around your office and talk to your colleagues (us
See more features for your virtual office: https://workadventu.re/virtual-office See more features for your virtual office: https://workadventu.re/virtual-office
## Community resources
Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure)
## Setting up a development environment ## Setting up a development environment
Install Docker. Install Docker.

View file

@ -12,43 +12,52 @@ export class DebugController {
getDump() { getDump() {
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery()); (async () => {
const query = parse(req.getQuery());
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!");
} }
return res return res
.writeStatus("200 OK") .writeStatus("200 OK")
.writeHeader("Content-Type", "application/json") .writeHeader("Content-Type", "application/json")
.end( .end(
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { stringify(
if (key === "listeners") { await Promise.all(socketManager.getWorlds().values()),
return "Listeners"; (key: unknown, value: unknown) => {
} if (key === "listeners") {
if (key === "socket") { return "Listeners";
return "Socket"; }
} if (key === "socket") {
if (key === "batchedMessages") { return "Socket";
return "BatchedMessages"; }
} if (key === "batchedMessages") {
if (value instanceof Map) { return "BatchedMessages";
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any }
for (const [mapKey, mapValue] of value.entries()) { if (value instanceof Map) {
obj[mapKey] = mapValue; const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
}
} }
return obj; )
} else if (value instanceof Set) { );
const obj: Array<unknown> = []; })().catch((e) => {
for (const [setKey, setValue] of value.entries()) { console.error(e);
obj.push(setValue); res.writeStatus("500");
} res.end("An error occurred");
return obj; });
} else {
return value;
}
})
);
}); });
} }
} }

View file

@ -97,6 +97,7 @@ export class SocketManager {
} }
const roomJoinedMessage = new RoomJoinedMessage(); const roomJoinedMessage = new RoomJoinedMessage();
roomJoinedMessage.setTagList(joinRoomMessage.getTagList()); roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken());
for (const [itemId, item] of room.getItemsState().entries()) { for (const [itemId, item] of room.getItemsState().entries()) {
const itemStateMessage = new ItemStateMessage(); const itemStateMessage = new ItemStateMessage();

View file

@ -1,12 +1,7 @@
/** /**
* Handles variables shared between the scripting API and the server. * Handles variables shared between the scripting API and the server.
*/ */
import { import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist";
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapObjectLayer,
} from "@workadventure/tiled-map-type-guard/dist";
import { User } from "_Model/User"; import { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository"; import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient"; import { redisClient } from "./RedisClient";

View file

@ -66,6 +66,7 @@ services:
API_URL: back:50051 API_URL: back:50051
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS JITSI_ISS: $JITSI_ISS
FRONT_URL: https://play.${DOMAIN}
labels: labels:
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)" - "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)"
- "traefik.http.routers.pusher.entryPoints=web,traefik" - "traefik.http.routers.pusher.entryPoints=web,traefik"

View file

@ -101,7 +101,10 @@
"host": { "host": {
"url": "maps-"+url "url": "maps-"+url
}, },
"ports": [80] "ports": [80],
"env": {
"FRONT_URL": "https://play-"+url
}
}, },
"redis": { "redis": {
"image": "redis:6", "image": "redis:6",

View file

@ -28,7 +28,7 @@ services:
NODE_ENV: development NODE_ENV: development
PUSHER_URL: /pusher PUSHER_URL: /pusher
UPLOADER_URL: /uploader UPLOADER_URL: /uploader
ADMIN_URL: /admin #ADMIN_URL: /admin
MAPS_URL: /maps MAPS_URL: /maps
ICON_URL: /icon ICON_URL: /icon
STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_1: ./templater.sh
@ -92,11 +92,12 @@ services:
- "traefik.http.routers.pusher-ssl.service=pusher" - "traefik.http.routers.pusher-ssl.service=pusher"
maps: maps:
image: thecodingmachine/nodejs:12-apache image: thecodingmachine/php:8.1-v4-apache-node12
environment: environment:
DEBUG_MODE: "$DEBUG_MODE" DEBUG_MODE: "$DEBUG_MODE"
HOST: "0.0.0.0" HOST: "0.0.0.0"
NODE_ENV: development NODE_ENV: development
FRONT_URL: http://play.workadventure.localhost
#APACHE_DOCUMENT_ROOT: dist/ #APACHE_DOCUMENT_ROOT: dist/
#APACHE_EXTENSIONS: headers #APACHE_EXTENSIONS: headers
#APACHE_EXTENSION_HEADERS: 1 #APACHE_EXTENSION_HEADERS: 1

View file

@ -1,12 +1,19 @@
version: "3" version: "3.5"
services: services:
testcafe: testcafe:
image: testcafe/testcafe:1.17.1 build: tests/
working_dir: /tests working_dir: /project/tests
command:
- --dev
# Run as root to have the right to access /var/run/docker.sock
user: root
environment: environment:
BROWSER: "chromium --use-fake-device-for-media-stream" BROWSER: "chromium --use-fake-device-for-media-stream"
PROJECT_DIR: ${PROJECT_DIR}
ADMIN_API_TOKEN: ${ADMIN_API_TOKEN}
volumes: volumes:
- ./tests:/tests - ./:/project
- ./maps:/maps - ./maps:/maps
- /var/run/docker.sock:/var/run/docker.sock
# security_opt: # security_opt:
# - seccomp:unconfined # - seccomp:unconfined

View file

@ -1,20 +1,21 @@
version: "3" version: "3.5"
services: services:
reverse-proxy: reverse-proxy:
image: traefik:v2.0 image: traefik:v2.5
command: command:
- --api.insecure=true - --api.insecure=true
- --providers.docker - --providers.docker
- --entryPoints.web.address=:80 - --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443 - --entryPoints.websecure.address=:443
- "--providers.docker.exposedbydefault=false"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
# The Web UI (enabled by --api.insecure=true) # The Web UI (enabled by --api.insecure=true)
- "8080:8080" - "8080:8080"
depends_on: #depends_on:
- back # - back
- front # - front
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
@ -34,7 +35,7 @@ services:
NODE_ENV: development NODE_ENV: development
PUSHER_URL: //pusher.workadventure.localhost PUSHER_URL: //pusher.workadventure.localhost
UPLOADER_URL: //uploader.workadventure.localhost UPLOADER_URL: //uploader.workadventure.localhost
ADMIN_URL: //workadventure.localhost #ADMIN_URL: //workadventure.localhost
ICON_URL: //icon.workadventure.localhost ICON_URL: //icon.workadventure.localhost
STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install STARTUP_COMMAND_2: yarn install
@ -51,10 +52,12 @@ services:
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS" DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER" OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
LIVE_RELOAD: "$LIVE_RELOAD:-true"
command: yarn run start command: yarn run start
volumes: volumes:
- ./front:/usr/src/app - ./front:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)" - "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front.entryPoints=web" - "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080" - "traefik.http.services.front.loadbalancer.server.port=8080"
@ -87,6 +90,7 @@ services:
volumes: volumes:
- ./pusher:/usr/src/app - ./pusher:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)" - "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)"
- "traefik.http.routers.pusher.entryPoints=web" - "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080" - "traefik.http.services.pusher.loadbalancer.server.port=8080"
@ -96,11 +100,12 @@ services:
- "traefik.http.routers.pusher-ssl.service=pusher" - "traefik.http.routers.pusher-ssl.service=pusher"
maps: maps:
image: thecodingmachine/nodejs:12-apache image: thecodingmachine/php:8.1-v4-apache-node12
environment: environment:
DEBUG_MODE: "$DEBUG_MODE" DEBUG_MODE: "$DEBUG_MODE"
HOST: "0.0.0.0" HOST: "0.0.0.0"
NODE_ENV: development NODE_ENV: development
FRONT_URL: http://play.workadventure.localhost
#APACHE_DOCUMENT_ROOT: dist/ #APACHE_DOCUMENT_ROOT: dist/
#APACHE_EXTENSIONS: headers #APACHE_EXTENSIONS: headers
#APACHE_EXTENSION_HEADERS: 1 #APACHE_EXTENSION_HEADERS: 1
@ -110,6 +115,7 @@ services:
volumes: volumes:
- ./maps:/var/www/html - ./maps:/var/www/html
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)" - "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)"
- "traefik.http.routers.maps.entryPoints=web,traefik" - "traefik.http.routers.maps.entryPoints=web,traefik"
- "traefik.http.services.maps.loadbalancer.server.port=80" - "traefik.http.services.maps.loadbalancer.server.port=80"
@ -141,6 +147,7 @@ services:
volumes: volumes:
- ./back:/usr/src/app - ./back:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)" - "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
- "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"
@ -159,6 +166,7 @@ services:
volumes: volumes:
- ./uploader:/usr/src/app - ./uploader:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)" - "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)"
- "traefik.http.routers.uploader.entryPoints=web" - "traefik.http.routers.uploader.entryPoints=web"
- "traefik.http.services.uploader.loadbalancer.server.port=8080" - "traefik.http.services.uploader.loadbalancer.server.port=8080"
@ -186,6 +194,7 @@ services:
redisinsight: redisinsight:
image: redislabs/redisinsight:latest image: redislabs/redisinsight:latest
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)" - "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
- "traefik.http.routers.redisinsight.entryPoints=web" - "traefik.http.routers.redisinsight.entryPoints=web"
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001" - "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
@ -197,6 +206,7 @@ services:
icon: icon:
image: matthiasluedtke/iconserver:v3.13.0 image: matthiasluedtke/iconserver:v3.13.0
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)" - "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
- "traefik.http.routers.icon.entryPoints=web" - "traefik.http.routers.icon.entryPoints=web"
- "traefik.http.services.icon.loadbalancer.server.port=8080" - "traefik.http.services.icon.loadbalancer.server.port=8080"

16
docs/dev/README.md Normal file
View file

@ -0,0 +1,16 @@
# Developer documentation
This (work in progress) documentation provides a number of "how-to" guides explaining how to work on the WorkAdventure
code.
This documentation is targeted at developers looking to open Pull Requests on WorkAdventure.
If you "only" want to design dynamic maps, please refer instead to the [scripting API documentation](https://workadventu.re/map-building/scripting.md).
## Contributing
Check out the [contributing guide](../../CONTRIBUTING.md)
## Front documentation
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)

View file

@ -0,0 +1,276 @@
# How to add new functions in the scripting API
This documentation is intended at contributors who want to participate in the development of WorkAdventure itself.
Before reading this, please be sure you are familiar with the [scripting API](https://workadventu.re/map-building/scripting.md).
The [scripting API](https://workadventu.re/map-building/scripting.md) allows map developers to add dynamic features in their maps.
## Why extend the scripting API?
The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to
offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance,
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that
a feature is missing in the API. This article is here to explain to you how to add this feature.
## How to extend the scripting API?
Extending the scripting API means modifying the core of WorkAdventure. You can of course run these
modifications on your self-hosted instance.
But if you want to share it with the wider community, I strongly encourage you to start by [opening an issue](https://github.com/thecodingmachine/workadventure/issues)
on GitHub before starting the development. Check with the core maintainers that they are willing to merge your idea
before starting developing it. Once a new function makes it into the scripting API, it is very difficult to make it
evolve (or to deprecate), so the design of the function you add needs to be carefully considered.
## How does it work?
Scripts are executed in the browser, inside an iframe.
![](images/scripting_1.svg)
The iframe allows WorkAdventure to isolate the script in a sandbox. Because the iframe is sandbox (or on a different
domain than the WorkAdventure server), scripts cannot directly manipulate the DOM of WorkAdventure. They also cannot
directly access Phaser objects (Phaser is the game engine used in WorkAdventure). This is by-design. Since anyone
can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be
a huge XSS security flaw).
Instead, the only way the script can interact with WorkAdventure is by sending messages using the
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
![](images/scripting_2.svg)
We want to make life easy for map developers. So instead of asking them to directly send messages using the postMessage
API, we provide a nice library that does this work for them. This library is what we call the "Scripting API" (we sometimes
refer to it as the "Client API").
The scripting API provides the global `WA` object.
## A simple example
So let's take an example with a sample script:
```typescript
WA.chat.sendChatMessage('Hello world!', 'John Doe');
```
When this script is called, the scripting API is dispatching a JSON message to WorkAdventure.
In our case, the `sendChatMessage` function looks like this:
**src/Api/iframe/chat.ts**
```typescript
sendChatMessage(message: string, author: string) {
sendToWorkadventure({
type: "chat",
data: {
message: message,
author: author,
},
});
}
```
The `sendToWorkadventure` function is a utility function that dispatches the message to the main frame.
In WorkAdventure, the message is received in the [`IframeListener` listener class](http://github.com/thecodingmachine/workadventure/blob/1e6ce4dec8697340e2c91798864b94da9528b482/front/src/Api/IframeListener.ts#L200-L203).
This class is in charge of analyzing the JSON messages received and dispatching them to the right place in the WorkAdventure application.
The message callback implemented in `IframeListener` is a giant (and disgusting) `if` statement branching to the correct
part of the code depending on the `type` property.
**src/Api/IframeListener.ts**
```typescript
// ...
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
scriptUtils.sendAnonymousChat(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
// ...
```
In this particular case, we call `scriptUtils.sendAnonymousChat` that is doing the work of displaying the chat message.
## Scripting API entry point
The `WA` object originates from the scripting API. This script is hosted on the front server, at `https://[front_WA_server]/iframe_api.js.`.
The entry point for this script is the file `front/src/iframe_api.ts`.
All the other files dedicated to the iframe API are located in the `src/Api/iframe` directory.
## Utility functions to exchange messages
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function.
Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes.
We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this.
Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function.
## Types
The JSON messages sent over the postMessage API are strictly defined using Typescript types.
Those types are not defined using classical Typescript interfaces.
Indeed, Typescript interfaces only exist at compilation time but cannot be enforced on runtime. The postMessage API
is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could
send specially crafted JSON packages to try to hack WA).
In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package
allows us to create interfaces AND custom type guards in one go.
Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API:
```typescript
sendToWorkadventure({
type: "chat",
data: {
message: message,
author: author,
},
});
```
The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`:
```typescript
import * as tg from "generic-type-guard";
export const isChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
author: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
```
Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`).
From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code.
The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received
over the postMessage API is indeed correctly formatted.
If you are not familiar with Typescript type guards, you can read [an introduction to type guards in the Typescript documentation](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards).
### Typing one way messages
For "one-way" messages (from the iframe to WorkAdventure), the `sendToWorkadventure` method expects the passed
object to be of type `IframeEvent<keyof IframeEventMap>`.
Note: I'd like here to thank @jonnytest1 for helping set up this type system. It rocks ;)
The `IFrameEvent` type is defined in `front/src/Api/Events/IframeEvent.ts`:
```typescript
export type IframeEventMap = {
loadPage: LoadPageEvent;
chat: ChatEvent;
openPopup: OpenPopupEvent;
closePopup: ClosePopupEvent;
openTab: OpenTabEvent;
// ...
// All the possible messages go here
// The key goes into the "type" JSON property
// ...
};
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
data: IframeEventMap[T];
}
```
Similarly, if you want to type messages from WorkAdventure to the iframe, there is a very similar `IframeResponseEvent`.
```typescript
export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent;
enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent;
// ...
// All the possible messages go here
// The key goes into the "type" JSON property
// ...
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T;
data: IframeResponseEventMap[T];
}
```
### Typing queries (messages with answers)
If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to
define the type of the query and the type of the response.
The signature of `queryWorkadventure` is:
```typescript
function queryWorkadventure<T extends keyof IframeQueryMap>(
content: IframeQuery<T>
): Promise<IframeQueryMap[T]["answer"]>
```
Yes, that's a bit cryptic. Hopefully, all you need to know is that to add a new query, you need to edit the `iframeQueryMapTypeGuards`
array in `front/src/Api/Events/IframeEvent.ts`:
```typescript
export const iframeQueryMapTypeGuards = {
openCoWebsite: {
query: isOpenCoWebsiteEvent,
answer: isCoWebsite,
},
getCoWebsites: {
query: tg.isUndefined,
answer: tg.isArray(isCoWebsite),
},
// ...
// the `query` key points to the type guard of the query
// the `answer` key points to the type guard of the response
};
```
### Responding to a query on the WorkAdventure side
In the WorkAdventure code, each possible query should be handled by what we call an "answerer".
Registering an answerer happens using the `iframeListener.registerAnswerer()` method.
Here is a sample:
```typescript
iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => {
// ...
return /*...*/;
});
```
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`).
Important:
- there can be only one answerer registered for a given query type.
- if the answerer is not valid any more, you need to unregister the answerer using `iframeListener.unregisterAnswerer`.
## sendToWorkadventure VS queryWorkadventure
- `sendToWorkadventure` is used to send messages one way from the iframe to WorkAdventure. No response is expected. In particular
if an error happens in WorkAdventure, the iframe will not be notified.
- `queryWorkadventure` is used to send queries that expect an answer. If an error happens in WorkAdventure (i.e. if an
exception is raised), the exception will be propagated to the iframe.
Because `queryWorkadventure` handles exceptions properly, it can be interesting to use `queryWorkadventure` instead
of `sendToWorkadventure`, even for "one-way" messages. The return message type is simply `undefined` in this case.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -79,6 +79,34 @@ WA.onInit().then(() => {
``` ```
### Get the user-room token of the player
```
WA.player.userRoomToken: string;
```
The user-room token is available from the `WA.player.userRoomToken` property.
This token can be used by third party services to authenticate a player and prove that the player is in a given room.
The token is generated by the administration panel linked to WorkAdventure. The token is a string and is depending on your implementation of the administration panel.
In WorkAdventure SAAS version, the token is a JWT token that contains information such as the player's room ID and its associated membership ID.
If you are using the self-hosted version of WorkAdventure and you developed your own administration panel, the token can be anything.
By default, self-hosted versions of WorkAdventure don't come with an administration panel, so the token string will be empty.
{.alert.alert-info}
A typical use-case for the user-room token is providing logo upload capabilities in a map.
The token can be used as a way to authenticate a WorkAdventure player and ensure he is indeed in the map and authorized to upload a logo.
{.alert.alert-info}
You need to wait for the end of the initialization before accessing `WA.player.userRoomToken`
```typescript
WA.onInit().then(() => {
console.log('Token: ', WA.player.userRoomToken);
})
```
### Listen to player movement ### Listen to player movement
``` ```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;

86
docs/maps/camera.md Normal file
View file

@ -0,0 +1,86 @@
{.section-title.accent.text-primary}
# Working with camera
## Focusable Zones
It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/0_focusable_zone.png" alt="" />
</div>
### Adding new **Focusable Zone**:
1. Make sure you are editing an **Object Layer**
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/1_object_layer.png" alt="" />
</div>
2. Select **Insert Rectangle** tool
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/2_rectangle_zone.png" alt="" />
</div>
3. Define new object wherever you want. For example, you can make your chilling room event cosier!
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/3_define_new_zone.png" alt="" />
</div>
4. Edit this new object and click on **Add Property**, like this:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/4_click_add_property.png" alt="" />
</div>
5. Add a **bool** property of name *focusable*:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/5_add_focusable_prop.png" alt="" />
</div>
6. Make sure it's checked! :)
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/6_make_sure_checked.png" alt="" />
</div>
All should be set up now and your new **Focusable Zone** should be working fine!
### Defining custom zoom margin:
If you want, you can add an additional property to control how much should the camera zoom onto focusable zone.
1. Like before, click on **Add Property**
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/4_click_add_property.png" alt="" />
</div>
2. Add a **float** property of name *zoom_margin*:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/7_add_zoom_margin.png" alt="" />
</div>
2. Define how much (in percentage value) should the zoom be decreased:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/8_optional_zoom_margin_defined.png" alt="" />
</div>
For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300.
- No margin defined
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/no_margin.png" alt="" />
</div>
- Margin set to **0.35**
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/with_margin.png" alt="" />
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -51,6 +51,12 @@ return [
'markdown' => 'maps.website-in-map', 'markdown' => 'maps.website-in-map',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md', 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md',
], ],
[
'title' => 'Camera',
'url' => '/map-building/camera.md',
'markdown' => 'maps.camera',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/camera.md',
],
[ [
'title' => 'Variables', 'title' => 'Variables',
'url' => '/map-building/variables.md', 'url' => '/map-building/variables.md',

View file

@ -92,3 +92,19 @@ You can add properties either on individual tiles of a tileset OR on a complete
If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer. If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer.
The exception is the "collides" property that can only be set on tiles, but not on a complete layer. The exception is the "collides" property that can only be set on tiles, but not on a complete layer.
## Insert helpful information in your map
By setting properties on the map itself, you can help visitors know more about the creators of the map.
The following *map* properties are supported:
* `mapName` (string)
* `mapDescription` (string)
* `mapCopyright` (string)
And *each tileset* can also have a property called `tilesetCopyright` (string).
Resulting in a "credit" page in the menu looking like this:
![](images/mapProperties.png){.document-img}

View file

@ -1,4 +1,4 @@
{ module.exports = {
"root": true, "root": true,
"env": { "env": {
"browser": true, "browser": true,
@ -18,10 +18,18 @@
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018, "ecmaVersion": 2018,
"sourceType": "module", "sourceType": "module",
"project": "./tsconfig.json" "project": "./tsconfig.json",
"extraFileExtensions": [".svelte"]
}, },
"plugins": [ "plugins": [
"@typescript-eslint" "@typescript-eslint",
"svelte3"
],
"overrides": [
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
}
], ],
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
@ -34,5 +42,9 @@
"@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off" "@typescript-eslint/restrict-template-expressions": "off"
},
"settings": {
"svelte3/typescript": true,
"svelte3/ignore-styles": () => true
} }
} }

View file

@ -1,4 +1,5 @@
{ {
"printWidth": 120, "printWidth": 120,
"tabWidth": 4 "tabWidth": 4,
"plugins": ["prettier-plugin-svelte"]
} }

View file

@ -11,6 +11,7 @@
const scriptUrl = urlParams.get('script'); const scriptUrl = urlParams.get('script');
const script = document.createElement('script'); const script = document.createElement('script');
script.src = scriptUrl; script.src = scriptUrl;
script.type = "module";
document.head.append(script); document.head.append(script);
</script> </script>
</head> </head>

View file

@ -16,6 +16,7 @@
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",
"css-loader": "^5.2.4", "css-loader": "^5.2.4",
"eslint": "^7.26.0", "eslint": "^7.26.0",
"eslint-plugin-svelte3": "^3.2.1",
"fork-ts-checker-webpack-plugin": "^6.2.9", "fork-ts-checker-webpack-plugin": "^6.2.9",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
@ -24,6 +25,7 @@
"node-polyfill-webpack-plugin": "^1.1.2", "node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"prettier-plugin-svelte": "^2.5.0",
"sass": "^1.32.12", "sass": "^1.32.12",
"sass-loader": "^11.1.0", "sass-loader": "^11.1.0",
"svelte": "^3.38.2", "svelte": "^3.38.2",
@ -40,7 +42,7 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/press-start-2p": "^4.3.0", "@fontsource/press-start-2p": "^4.3.0",
"@joeattardi/emoji-button": "^4.6.0", "@joeattardi/emoji-button": "^4.6.2",
"@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",
@ -54,6 +56,7 @@
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
"quill-delta-to-html": "^0.12.0", "quill-delta-to-html": "^0.12.0",
"retry-axios": "^2.6.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"simple-peer": "^9.11.0", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
@ -67,17 +70,21 @@
"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",
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack", "build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
"test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "lint": "node_modules/.bin/eslint src/ tests/ --ext .ts,.svelte",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ tests/ --ext .ts,.svelte",
"precommit": "lint-staged", "precommit": "lint-staged",
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
"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,tsx}'", "pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" "pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'"
}, },
"lint-staged": { "lint-staged": {
"*.ts": [ "*.svelte": [
"prettier --write" "yarn run svelte-check"
],
"*.{ts,svelte}": [
"yarn run fix",
"yarn run pretty"
] ]
} }
} }

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isChangeZoneEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone.
*/
export type ChangeZoneEvent = tg.GuardedType<typeof isChangeZoneEvent>;

View file

@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface()
tags: tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
variables: tg.isObject, variables: tg.isObject,
playerVariables: tg.isObject, playerVariables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
}) })
.get(); .get();
/** /**

View file

@ -30,6 +30,7 @@ import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEv
import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import { isPlayerPosition } from "./PlayerPosition"; import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -79,6 +80,8 @@ export interface IframeResponseEventMap {
leaveEvent: EnterLeaveEvent; leaveEvent: EnterLeaveEvent;
enterLayerEvent: ChangeLayerEvent; enterLayerEvent: ChangeLayerEvent;
leaveLayerEvent: ChangeLayerEvent; leaveLayerEvent: ChangeLayerEvent;
enterZoneEvent: ChangeZoneEvent;
leaveZoneEvent: ChangeZoneEvent;
buttonClickedEvent: ButtonClickedEvent; buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent; hasPlayerMoved: HasPlayerMovedEvent;
wasCameraUpdated: WasCameraUpdatedEvent; wasCameraUpdated: WasCameraUpdatedEvent;

View file

@ -32,6 +32,7 @@ import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/Emb
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"], query: IframeQueryMap[T]["query"],
@ -317,7 +318,7 @@ class IframeListener {
"//" + "//" +
window.location.host + window.location.host +
'/iframe_api.js" ></script>\n' + '/iframe_api.js" ></script>\n' +
'<script src="' + '<script type="module" src="' +
scriptUrl + scriptUrl +
'" ></script>\n' + '" ></script>\n' +
"<title></title>\n" + "<title></title>\n" +
@ -420,6 +421,24 @@ class IframeListener {
}); });
} }
sendEnterZoneEvent(zoneName: string) {
this.postMessage({
type: "enterZoneEvent",
data: {
name: zoneName,
} as ChangeZoneEvent,
});
}
sendLeaveZoneEvent(zoneName: string) {
this.postMessage({
type: "leaveZoneEvent",
data: {
name: zoneName,
} as ChangeZoneEvent,
});
}
hasPlayerMoved(event: HasPlayerMovedEvent) { hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) { if (this.sendPlayerMove) {
this.postMessage({ this.postMessage({

View file

@ -21,6 +21,12 @@ export const setTags = (_tags: string[]) => {
let uuid: string | undefined; let uuid: string | undefined;
let userRoomToken: string | undefined;
export const setUserRoomToken = (token: string | undefined) => {
userRoomToken = token;
};
export const setUuid = (_uuid: string | undefined) => { export const setUuid = (_uuid: string | undefined) => {
uuid = _uuid; uuid = _uuid;
}; };
@ -77,6 +83,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
data: undefined, data: undefined,
}); });
} }
get userRoomToken(): string | undefined {
if (userRoomToken === undefined) {
throw new Error(
"User-room token not initialized yet. You should call WA.player.userRoomToken within a WA.onInit callback."
);
}
return userRoomToken;
}
} }
export type Position = { export type Position = {

View file

@ -1,147 +1,146 @@
<script lang="typescript"> <script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte"; import MenuIcon from "./Menu/MenuIcon.svelte";
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../Stores/MenuStore"; import { menuIconVisiblilityStore, menuVisiblilityStore } from "../Stores/MenuStore";
import {emoteMenuStore} from "../Stores/EmoteStore"; import { emoteMenuStore } from "../Stores/EmoteStore";
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore"; import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte"; import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte"; import MyCamera from "./MyCamera.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte"; import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import {selectCompanionSceneVisibleStore} from "../Stores/SelectCompanionStore"; import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import {selectCharacterSceneVisibleStore} from "../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte"; import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import {customCharacterSceneVisibleStore} from "../Stores/CustomCharacterStore"; import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import {errorStore} from "../Stores/ErrorStore"; import { errorStore } from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte"; import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte"; import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte"; import Chat from "./Chat/Chat.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore"; import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte"; import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte"; import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore"; 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 { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte"; import Menu from "./Menu/Menu.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte"; import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte"; import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import AdminMessage from "./TypeMessage/BanMessage.svelte"; import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte"; import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore"; import { banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore"; import { textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
import {warningContainerStore} from "../Stores/MenuStore"; import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte"; import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore"; import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import LayoutManager from "./LayoutManager/LayoutManager.svelte"; import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore"; import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte" import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore"; import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte"; import ReportMenu from "./ReportMenu/ReportMenu.svelte";
export let game: Game; export let game: Game;
</script> </script>
<div> <div>
{#if $loginSceneVisibleStore} {#if $loginSceneVisibleStore}
<div class="scrollable"> <div class="scrollable">
<LoginScene game={game}></LoginScene> <LoginScene {game} />
</div> </div>
{/if} {/if}
{#if $selectCharacterSceneVisibleStore} {#if $selectCharacterSceneVisibleStore}
<div> <div>
<SelectCharacterScene game={ game }></SelectCharacterScene> <SelectCharacterScene {game} />
</div> </div>
{/if} {/if}
{#if $customCharacterSceneVisibleStore} {#if $customCharacterSceneVisibleStore}
<div> <div>
<CustomCharacterScene game={ game }></CustomCharacterScene> <CustomCharacterScene {game} />
</div> </div>
{/if} {/if}
{#if $selectCompanionSceneVisibleStore} {#if $selectCompanionSceneVisibleStore}
<div> <div>
<SelectCompanionScene game={ game }></SelectCompanionScene> <SelectCompanionScene {game} />
</div> </div>
{/if} {/if}
{#if $enableCameraSceneVisibilityStore} {#if $enableCameraSceneVisibilityStore}
<div class="scrollable"> <div class="scrollable">
<EnableCameraScene game={game}></EnableCameraScene> <EnableCameraScene {game} />
</div> </div>
{/if} {/if}
{#if $banMessageVisibleStore} {#if $banMessageVisibleStore}
<div> <div>
<AdminMessage></AdminMessage> <AdminMessage />
</div> </div>
{/if} {/if}
{#if $textMessageVisibleStore} {#if $textMessageVisibleStore}
<div> <div>
<TextMessage></TextMessage> <TextMessage />
</div> </div>
{/if} {/if}
{#if $soundPlayingStore} {#if $soundPlayingStore}
<div> <div>
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
</div> </div>
{/if} {/if}
{#if $audioManagerVisibilityStore} {#if $audioManagerVisibilityStore}
<div> <div>
<AudioManager></AudioManager> <AudioManager />
</div> </div>
{/if} {/if}
{#if $layoutManagerVisibilityStore} {#if $layoutManagerVisibilityStore}
<div> <div>
<LayoutManager></LayoutManager> <LayoutManager />
</div> </div>
{/if} {/if}
{#if $showReportScreenStore !== userReportEmpty} {#if $showReportScreenStore !== userReportEmpty}
<div> <div>
<ReportMenu></ReportMenu> <ReportMenu />
</div> </div>
{/if} {/if}
{#if $menuIconVisiblilityStore} {#if $menuIconVisiblilityStore}
<div> <div>
<MenuIcon></MenuIcon> <MenuIcon />
</div> </div>
{/if} {/if}
{#if $menuVisiblilityStore} {#if $menuVisiblilityStore}
<div> <div>
<Menu></Menu> <Menu />
</div> </div>
{/if} {/if}
{#if $emoteMenuStore} {#if $emoteMenuStore}
<div> <div>
<EmoteMenu></EmoteMenu> <EmoteMenu />
</div> </div>
{/if} {/if}
{#if $gameOverlayVisibilityStore} {#if $gameOverlayVisibilityStore}
<div> <div>
<VideoOverlay></VideoOverlay> <VideoOverlay />
<MyCamera></MyCamera> <MyCamera />
<CameraControls></CameraControls> <CameraControls />
</div> </div>
{/if} {/if}
{#if $helpCameraSettingsVisibleStore} {#if $helpCameraSettingsVisibleStore}
<div> <div>
<HelpCameraSettingsPopup></HelpCameraSettingsPopup> <HelpCameraSettingsPopup />
</div> </div>
{/if} {/if}
{#if $requestVisitCardsStore} {#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore}></VisitCard> <VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if} {/if}
{#if $errorStore.length > 0} {#if $errorStore.length > 0}
<div> <div>
<ErrorDialog></ErrorDialog> <ErrorDialog />
</div> </div>
{/if} {/if}
{#if $chatVisibilityStore} {#if $chatVisibilityStore}
<Chat></Chat> <Chat />
{/if} {/if}
{#if $warningContainerStore} {#if $warningContainerStore}
<WarningContainer></WarningContainer> <WarningContainer />
{/if} {/if}
</div> </div>

View file

@ -1,10 +1,7 @@
<script lang="ts"> <script lang="ts">
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore"; import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import { import { audioManagerFileStore, audioManagerVolumeStore } from "../../Stores/AudioManagerStore";
audioManagerFileStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
@ -15,15 +12,12 @@
let unsubscriberFileStore: Unsubscriber | null = null; let unsubscriberFileStore: Unsubscriber | null = null;
let unsubscriberVolumeStore: Unsubscriber | null = null; let unsubscriberVolumeStore: Unsubscriber | null = null;
let volume: number = 1;
let decreaseWhileTalking: boolean = true; let decreaseWhileTalking: boolean = true;
onMount(() => { onMount(() => {
volume = localUserStore.getAudioPlayerVolume(); let volume = Math.min(localUserStore.getAudioPlayerVolume(), get(audioManagerVolumeStore).volume);
audioManagerVolumeStore.setVolume(volume);
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted()); audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
changeVolume();
loadAudioSettings();
unsubscriberFileStore = audioManagerFileStore.subscribe(() => { unsubscriberFileStore = audioManagerFileStore.subscribe(() => {
HTMLAudioPlayer.pause(); HTMLAudioPlayer.pause();
@ -43,8 +37,9 @@
HTMLAudioPlayer.volume = audioManager.volume; HTMLAudioPlayer.volume = audioManager.volume;
HTMLAudioPlayer.muted = audioManager.muted; HTMLAudioPlayer.muted = audioManager.muted;
HTMLAudioPlayer.loop = audioManager.loop; HTMLAudioPlayer.loop = audioManager.loop;
}) updateVolumeUI();
}) });
});
onDestroy(() => { onDestroy(() => {
if (unsubscriberFileStore) { if (unsubscriberFileStore) {
@ -53,23 +48,24 @@
if (unsubscriberVolumeStore) { if (unsubscriberVolumeStore) {
unsubscriberVolumeStore(); unsubscriberVolumeStore();
} }
}) });
function changeVolume() { function updateVolumeUI() {
if (get(audioManagerVolumeStore).muted) { if (get(audioManagerVolumeStore).muted) {
audioPlayerVolumeIcon.classList.add('muted'); audioPlayerVolumeIcon.classList.add("muted");
audioPlayerVol.value = "0"; audioPlayerVol.value = "0";
} else { } else {
let volume = HTMLAudioPlayer.volume;
audioPlayerVol.value = "" + volume; audioPlayerVol.value = "" + volume;
audioPlayerVolumeIcon.classList.remove('muted'); audioPlayerVolumeIcon.classList.remove("muted");
if (volume < 0.3) { if (volume < 0.3) {
audioPlayerVolumeIcon.classList.add('low'); audioPlayerVolumeIcon.classList.add("low");
} else if (volume < 0.7) { } else if (volume < 0.7) {
audioPlayerVolumeIcon.classList.remove('low'); audioPlayerVolumeIcon.classList.remove("low");
audioPlayerVolumeIcon.classList.add('mid'); audioPlayerVolumeIcon.classList.add("mid");
} else { } else {
audioPlayerVolumeIcon.classList.remove('low'); audioPlayerVolumeIcon.classList.remove("low");
audioPlayerVolumeIcon.classList.remove('mid'); audioPlayerVolumeIcon.classList.remove("mid");
} }
} }
} }
@ -78,21 +74,14 @@
const muted = !get(audioManagerVolumeStore).muted; const muted = !get(audioManagerVolumeStore).muted;
audioManagerVolumeStore.setMuted(muted); audioManagerVolumeStore.setMuted(muted);
localUserStore.setAudioPlayerMuted(muted); localUserStore.setAudioPlayerMuted(muted);
changeVolume();
}
function loadAudioSettings() {
audioManagerVolumeStore.setVolume(localUserStore.getAudioPlayerVolume());
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
} }
function setVolume() { function setVolume() {
volume = parseFloat(audioPlayerVol.value); let volume = parseFloat(audioPlayerVol.value);
audioManagerVolumeStore.setVolume(volume); audioManagerVolumeStore.setVolume(volume);
localUserStore.setAudioPlayerVolume(volume); localUserStore.setAudioPlayerVolume(volume);
audioManagerVolumeStore.setMuted(false); audioManagerVolumeStore.setMuted(false);
localUserStore.setAudioPlayerMuted(false); localUserStore.setAudioPlayerMuted(false);
changeVolume();
} }
function disallowKeys() { function disallowKeys() {
@ -105,45 +94,67 @@
} }
</script> </script>
<div class="main-audio-manager nes-container is-rounded"> <div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume"> <div class="audio-manager-player-volume">
<span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon} <span
on:click={onMute}> id="audioplayer_volume_icon_playing"
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" alt="player volume"
xmlns="http://www.w3.org/2000/svg"> bind:this={audioPlayerVolumeIcon}
<path fill-rule="evenodd" on:click={onMute}
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" /> >
<svg
width="2em"
height="2em"
viewBox="0 0 16 16"
class="bi bi-volume-up"
fill="white"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z"
/>
<g id="audioplayer_volume_icon_playing_high"> <g id="audioplayer_volume_icon_playing_high">
<path <path
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" /> d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"
/>
</g> </g>
<g id="audioplayer_volume_icon_playing_mid"> <g id="audioplayer_volume_icon_playing_mid">
<path <path
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" /> d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"
/>
</g> </g>
<g id="audioplayer_volume_icon_playing_low"> <g id="audioplayer_volume_icon_playing_low">
<path <path
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" /> d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z"
/>
</g> </g>
</svg> </svg>
</span> </span>
<input type="range" min="0" max="1" step="0.025" bind:this={audioPlayerVol} on:change={setVolume} on:keydown={disallowKeys}> <input
type="range"
min="0"
max="1"
step="0.025"
bind:this={audioPlayerVol}
on:change={setVolume}
on:keydown={disallowKeys}
/>
</div> </div>
<div class="audio-manager-reduce-conversation"> <div class="audio-manager-reduce-conversation">
<label> <label>
reduce in conversations reduce in conversations
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease}> <input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
</label> </label>
<section class="audio-manager-file"> <section class="audio-manager-file">
<!-- svelte-ignore a11y-media-has-caption -->
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}> <audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
<source src={$audioManagerFileStore}> <source src={$audioManagerFileStore} />
</audio> </audio>
</section> </section>
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
div.main-audio-manager.nes-container.is-rounded { div.main-audio-manager.nes-container.is-rounded {
position: relative; position: relative;
@ -208,4 +219,4 @@
display: none; display: none;
} }
} }
</style> </style>

View file

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

View file

@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from "svelte/transition";
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore"; import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte'; import ChatMessageForm from "./ChatMessageForm.svelte";
import ChatElement from './ChatElement.svelte'; import ChatElement from "./ChatElement.svelte";
import {afterUpdate, beforeUpdate, onMount} from "svelte"; import { afterUpdate, beforeUpdate, onMount } from "svelte";
import {HtmlUtils} from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
let listDom: HTMLElement; let listDom: HTMLElement;
let chatWindowElement: HTMLElement; let chatWindowElement: HTMLElement;
let handleFormBlur: { blur():void }; let handleFormBlur: { blur(): void };
let autoscroll: boolean; let autoscroll: boolean;
beforeUpdate(() => { beforeUpdate(() => {
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20); autoscroll = listDom && listDom.offsetHeight + listDom.scrollTop > listDom.scrollHeight - 20;
}); });
onMount(() => { onMount(() => {
listDom.scrollTo(0, listDom.scrollHeight); listDom.scrollTo(0, listDom.scrollHeight);
}) });
afterUpdate(() => { afterUpdate(() => {
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight); if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
@ -32,83 +32,82 @@
function closeChat() { function closeChat() {
chatVisibilityStore.set(false); chatVisibilityStore.set(false);
} }
function onKeyDown(e:KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') { if (e.key === "Escape") {
closeChat(); closeChat();
} }
} }
</script> </script>
<svelte:window on:keydown={onKeyDown} on:click={onClick}/> <svelte:window on:keydown={onKeyDown} on:click={onClick} />
<aside class="chatWindow" transition:fly={{ x: -1000, duration: 500 }} bind:this={chatWindowElement}>
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}" bind:this={chatWindowElement}>
<p class="close-icon" 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">Here is your chat history: </p></li> <li><p class="system-text">Here is your chat history:</p></li>
{#each $chatMessagesStore as message, i} {#each $chatMessagesStore as message, i}
<li><ChatElement message={message} line={i}></ChatElement></li> <li><ChatElement {message} line={i} /></li>
{/each} {/each}
</ul> </ul>
</section> </section>
<section class="messageForm"> <section class="messageForm">
<ChatMessageForm bind:handleForm={handleFormBlur}></ChatMessageForm> <ChatMessageForm bind:handleForm={handleFormBlur} />
</section> </section>
</aside> </aside>
<style lang="scss"> <style lang="scss">
p.close-icon { p.close-icon {
position: absolute; position: absolute;
padding: 4px; padding: 4px;
right: 12px; right: 12px;
font-size: 30px; font-size: 30px;
line-height: 25px; line-height: 25px;
cursor: pointer; cursor: pointer;
} }
p.system-text { p.system-text {
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
padding:6px; padding: 6px;
overflow-wrap: break-word; overflow-wrap: break-word;
max-width: 100%; max-width: 100%;
background: gray; background: gray;
display: inline-block; display: inline-block;
} }
aside.chatWindow { aside.chatWindow {
z-index:100; z-index: 100;
pointer-events: auto; pointer-events: auto;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100vh; height: 100vh;
width:30vw; width: 30vw;
min-width: 350px; min-width: 350px;
background: rgb(5, 31, 51, 0.9); background: rgb(5, 31, 51, 0.9);
color: whitesmoke; color: whitesmoke;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; padding: 10px;
border-bottom-right-radius: 16px;
border-top-right-radius: 16px;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
ul { border-bottom-right-radius: 16px;
list-style-type: none; border-top-right-radius: 16px;
padding-left: 0;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
ul {
list-style-type: none;
padding-left: 0;
}
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
} }
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
}
} }
</style> </style>

View file

@ -1,45 +1,53 @@
<script lang="ts"> <script lang="ts">
import {ChatMessageTypes} from "../../Stores/ChatStore"; import { ChatMessageTypes } from "../../Stores/ChatStore";
import type {ChatMessage} from "../../Stores/ChatStore"; import type { ChatMessage } from "../../Stores/ChatStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import ChatPlayerName from './ChatPlayerName.svelte'; import ChatPlayerName from "./ChatPlayerName.svelte";
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
export let message: ChatMessage; export let message: ChatMessage;
export let line: number; export let line: number;
const chatStyleLink = "color: white; text-decoration: underline;"; const chatStyleLink = "color: white; text-decoration: underline;";
$: author = message.author as PlayerInterface; $: author = message.author as PlayerInterface;
$: targets = message.targets || []; $: targets = message.targets || [];
$: texts = message.text || []; $: texts = message.text || [];
function urlifyText(text: string): string { function urlifyText(text: string): string {
return HtmlUtils.urlify(text, chatStyleLink); return HtmlUtils.urlify(text, chatStyleLink);
} }
function renderDate(date: Date) { function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, { return date.toLocaleTimeString(navigator.language, {
hour: '2-digit', hour: "2-digit",
minute:'2-digit' minute: "2-digit",
}); });
} }
function isLastIteration(index: number) { function isLastIteration(index: number) {
return targets.length -1 === index; return targets.length - 1 === index;
} }
</script> </script>
<div class="chatElement"> <div class="chatElement">
<div class="messagePart"> <div class="messagePart">
{#if message.type === ChatMessageTypes.userIncoming} {#if message.type === ChatMessageTypes.userIncoming}
&gt;&gt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span> &gt;&gt; {#each targets as target, index}<ChatPlayerName
player={target}
{line}
/>{#if !isLastIteration(index)}, {/if}{/each} entered
<span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.userOutcoming} {:else if message.type === ChatMessageTypes.userOutcoming}
&lt;&lt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span> &lt;&lt; {#each targets as target, index}<ChatPlayerName
player={target}
{line}
/>{#if !isLastIteration(index)}, {/if}{/each} left
<span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.me} {:else if message.type === ChatMessageTypes.me}
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4> <h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text} {#each texts as text}
<div><p class="my-text">{@html urlifyText(text)}</p></div> <div><p class="my-text">{@html urlifyText(text)}</p></div>
{/each} {/each}
{:else} {:else}
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4> <h4><ChatPlayerName player={author} {line} />: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text} {#each texts as text}
<div><p class="other-text">{@html urlifyText(text)}</p></div> <div><p class="other-text">{@html urlifyText(text)}</p></div>
{/each} {/each}
@ -48,37 +56,38 @@
</div> </div>
<style lang="scss"> <style lang="scss">
h4, p { h4,
p {
font-family: Lato; font-family: Lato;
} }
div.chatElement { div.chatElement {
display: flex; display: flex;
margin-bottom: 20px; margin-bottom: 20px;
.messagePart {
flex-grow:1;
max-width: 100%;
span.date { .messagePart {
font-size: 80%; flex-grow: 1;
color: gray; max-width: 100%;
}
div > p { span.date {
border-radius: 8px; font-size: 80%;
margin-bottom: 10px; color: gray;
padding:6px; }
overflow-wrap: break-word;
max-width: 100%; div > p {
display: inline-block; border-radius: 8px;
&.other-text { margin-bottom: 10px;
background: gray; padding: 6px;
} overflow-wrap: break-word;
max-width: 100%;
&.my-text { display: inline-block;
background: #6489ff; &.other-text {
} background: gray;
}
&.my-text {
background: #6489ff;
}
}
} }
}
} }
</style> </style>

View file

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore"; import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore";
export const handleForm = { export const handleForm = {
blur() { blur() {
inputElement.blur(); inputElement.blur();
} },
} };
let inputElement: HTMLElement; let inputElement: HTMLElement;
let newMessageText = ''; let newMessageText = "";
function onFocus() { function onFocus() {
chatInputFocusStore.set(true); chatInputFocusStore.set(true);
} }
@ -19,14 +19,21 @@
function saveMessage() { function saveMessage() {
if (!newMessageText) return; if (!newMessageText) return;
chatMessagesStore.addPersonnalMessage(newMessageText); chatMessagesStore.addPersonnalMessage(newMessageText);
newMessageText = ''; newMessageText = "";
} }
</script> </script>
<form on:submit|preventDefault={saveMessage}> <form on:submit|preventDefault={saveMessage}>
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} bind:this={inputElement}> <input
type="text"
bind:value={newMessageText}
placeholder="Enter your message..."
on:focus={onFocus}
on:blur={onBlur}
bind:this={inputElement}
/>
<button type="submit"> <button type="submit">
<img src="/static/images/send.png" alt="Send" width="20"> <img src="/static/images/send.png" alt="Send" width="20" />
</button> </button>
</form> </form>
@ -35,28 +42,28 @@
display: flex; display: flex;
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;
input { input {
flex: auto; flex: auto;
background-color: #254560; background-color: #254560;
color: white; color: white;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border: none; border: none;
font-size: 22px; font-size: 22px;
font-family: Lato; font-family: Lato;
padding-left: 6px; padding-left: 6px;
min-width: 0; //Needed so that the input doesn't overflow the container in firefox min-width: 0; //Needed so that the input doesn't overflow the container in firefox
outline: none; outline: none;
} }
button { button {
background-color: #254560; background-color: #254560;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
border: none; border: none;
border-left: solid white 1px; border-left: solid white 1px;
font-size: 16px; font-size: 16px;
} }
} }
</style> </style>

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import {chatSubMenuVisibilityStore} from "../../Stores/ChatStore"; import { chatSubMenuVisibilityStore } from "../../Stores/ChatStore";
import {onDestroy, onMount} from "svelte"; import { onDestroy, onMount } from "svelte";
import type {Unsubscriber} from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import ChatSubMenu from "./ChatSubMenu.svelte"; import ChatSubMenu from "./ChatSubMenu.svelte";
export let player: PlayerInterface; export let player: PlayerInterface;
@ -17,35 +17,33 @@
onMount(() => { onMount(() => {
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisibilityStore.subscribe((newValue) => { chatSubMenuVisivilytUnsubcribe = chatSubMenuVisibilityStore.subscribe((newValue) => {
isSubMenuOpen = (newValue === player.name + line); isSubMenuOpen = newValue === player.name + line;
}) });
}) });
onDestroy(() => { onDestroy(() => {
chatSubMenuVisivilytUnsubcribe(); chatSubMenuVisivilytUnsubcribe();
}) });
</script> </script>
<span class="subMenu"> <span class="subMenu">
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}> <span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
{player.name} {player.name}
</span> </span>
{#if isSubMenuOpen} {#if isSubMenuOpen}
<ChatSubMenu player={player}/> <ChatSubMenu {player} />
{/if} {/if}
</span> </span>
<style lang="scss"> <style lang="scss">
span.subMenu { span.subMenu {
display: inline-block; display: inline-block;
}
span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
} }
</style> span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
}
</style>

View file

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import {requestVisitCardsStore} from "../../Stores/GameStore"; import { requestVisitCardsStore } from "../../Stores/GameStore";
export let player: PlayerInterface; export let player: PlayerInterface;
function openVisitCard() { function openVisitCard() {
if (player.visitCardUrl) { if (player.visitCardUrl) {
requestVisitCardsStore.set(player.visitCardUrl); requestVisitCardsStore.set(player.visitCardUrl);
@ -17,17 +16,16 @@
<li><button class="text-btn" disabled>Add friend</button></li> <li><button class="text-btn" disabled>Add friend</button></li>
</ul> </ul>
<style lang="scss"> <style lang="scss">
ul.selectMenu { ul.selectMenu {
background-color: whitesmoke; background-color: whitesmoke;
position: absolute; position: absolute;
padding: 5px; padding: 5px;
border-radius: 4px; border-radius: 4px;
list-style-type: none; list-style-type: none;
li { li {
text-align: center; text-align: center;
}
} }
} </style>
</style>

View file

@ -0,0 +1,50 @@
<script lang="typescript">
import { gameManager } from "../../Phaser/Game/GameManager";
import type { PictureStore } from "../../Stores/PictureStore";
import { onDestroy } from "svelte";
export let userId: number;
export let placeholderSrc: string;
export let width: string = "62px";
export let height: string = "62px";
const gameScene = gameManager.getCurrentGameScene();
let companionWokaPictureStore: PictureStore | undefined;
if (userId === -1) {
companionWokaPictureStore = gameScene.CurrentPlayer.companion?.pictureStore;
} else {
companionWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(
userId,
(item) => item.companion?.pictureStore
);
}
let src = placeholderSrc;
if (companionWokaPictureStore) {
const unsubscribe = companionWokaPictureStore.subscribe((source) => {
src = source ?? placeholderSrc;
});
onDestroy(unsubscribe);
}
</script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
<style>
img {
display: inline-block;
pointer-events: auto;
width: var(--theme-width);
height: var(--theme-height);
margin: 0;
padding: 0;
position: static;
left: 0;
bottom: 0;
right: 0;
top: 0;
image-rendering: pixelated;
}
</style>

View file

@ -1,7 +1,7 @@
<script lang="typescript"> <script lang="typescript">
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene"; import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
import {activeRowStore} from "../../Stores/CustomCharacterStore"; import { activeRowStore } from "../../Stores/CustomCharacterStore";
export let game: Game; export let game: Game;
@ -30,7 +30,6 @@
function finish() { function finish() {
customCharacterScene.nextSceneToCamera(); customCharacterScene.nextSceneToCamera();
} }
</script> </script>
<form class="customCharacterScene"> <form class="customCharacterScene">
@ -38,80 +37,100 @@
<h2>Customize your WOKA</h2> <h2>Customize your WOKA</h2>
</section> </section>
<section class="action action-move"> <section class="action action-move">
<button class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> &lt; </button> <button
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button> class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn"
on:click|preventDefault={selectLeft}
>
&lt;
</button>
<button
class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn"
on:click|preventDefault={selectRight}
>
&gt;
</button>
</section> </section>
<section class="action"> <section class="action">
{#if $activeRowStore === 0} {#if $activeRowStore === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button> <button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene}
>Return</button
>
{/if} {/if}
{#if $activeRowStore !== 0} {#if $activeRowStore !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button> <button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp}
>Back <img src="resources/objects/arrow_up_black.png" alt="" /></button
>
{/if} {/if}
{#if $activeRowStore === 5} {#if $activeRowStore === 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button> <button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={finish}>Finish</button
>
{/if} {/if}
{#if $activeRowStore !== 5} {#if $activeRowStore !== 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ selectDown }>Next <img src="resources/objects/arrow_down.png" alt=""/></button> <button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectDown}>Next <img src="resources/objects/arrow_down.png" alt="" /></button
>
{/if} {/if}
</section> </section>
</form> </form>
<style lang="scss"> <style lang="scss">
form.customCharacterScene { form.customCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
margin: 1px; pointer-events: auto;
} color: #ebeeee;
&.text-center { section {
text-align: center; margin: 10px;
}
button.customCharacterSceneButton { &.action {
position: absolute; text-align: center;
top: 33vh; margin-top: 55vh;
margin: 0; }
}
button.customCharacterSceneFormBack { h2 {
color: #292929; font-family: "Press Start 2P";
} margin: 1px;
}
&.text-center {
text-align: center;
}
button.customCharacterSceneButton {
position: absolute;
top: 33vh;
margin: 0;
}
button.customCharacterSceneFormBack {
color: #292929;
}
}
button {
font-family: "Press Start 2P";
&.customCharacterSceneButtonLeft {
left: 33vw;
}
&.customCharacterSceneButtonRight {
right: 33vw;
}
}
} }
button { @media only screen and (max-width: 800px) {
font-family: "Press Start 2P"; form.customCharacterScene button.customCharacterSceneButtonLeft {
left: 5vw;
&.customCharacterSceneButtonLeft { }
left: 33vw; form.customCharacterScene button.customCharacterSceneButtonRight {
} right: 5vw;
}
&.customCharacterSceneButtonRight {
right: 33vw;
}
} }
}
@media only screen and (max-width: 800px) {
form.customCharacterScene button.customCharacterSceneButtonLeft{
left: 5vw;
}
form.customCharacterScene button.customCharacterSceneButtonRight{
right: 5vw;
}
}
</style> </style>

View file

@ -1,75 +1,72 @@
<script lang="typescript"> <script lang="typescript">
import type { Unsubscriber } from "svelte/store";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { onDestroy, onMount } from "svelte";
import { EmojiButton } from "@joeattardi/emoji-button";
import { isMobile } from "../../Enum/EnvironmentVariable";
import type { Unsubscriber } from "svelte/store"; let emojiContainer: HTMLElement;
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; let picker: EmojiButton;
import { onDestroy, onMount } from "svelte";
import { EmojiButton } from '@joeattardi/emoji-button';
import { isMobile } from "../../Enum/EnvironmentVariable";
let emojiContainer: HTMLElement; let unsubscriber: Unsubscriber | null = null;
let picker: EmojiButton;
let unsubscriber: Unsubscriber | null = null; onMount(() => {
picker = new EmojiButton({
rootElement: emojiContainer,
styleProperties: {
"--font": "Press Start 2P",
},
emojisPerRow: isMobile() ? 6 : 8,
autoFocusSearch: false,
style: "twemoji",
});
//the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100);
onMount(() => { picker.on("emoji", (selection) => {
picker = new EmojiButton({ emoteStore.set({
rootElement: emojiContainer, unicode: selection.emoji,
styleProperties: { url: selection.url,
'--font': 'Press Start 2P' name: selection.name,
}, });
emojisPerRow: isMobile() ? 6 : 8, });
autoFocusSearch: false,
style: 'twemoji',
});
//the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100);
picker.on("emoji", (selection) => { picker.on("hidden", () => {
emoteStore.set({ emoteMenuStore.closeEmoteMenu();
unicode: selection.emoji, });
url: selection.url,
name: selection.name
});
}); });
picker.on("hidden", () => { function onKeyDown(e: KeyboardEvent) {
emoteMenuStore.closeEmoteMenu(); if (e.key === "Escape") {
}); emoteMenuStore.closeEmoteMenu();
}
})
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
emoteMenuStore.closeEmoteMenu();
}
}
onDestroy(() => {
if (unsubscriber) {
unsubscriber();
} }
picker.destroyPicker(); onDestroy(() => {
}) if (unsubscriber) {
unsubscriber();
}
picker.destroyPicker();
});
</script> </script>
<svelte:window on:keydown={onKeyDown}/> <svelte:window on:keydown={onKeyDown} />
<div class="emote-menu-container"> <div class="emote-menu-container">
<div class="emote-menu" bind:this={emojiContainer}></div> <div class="emote-menu" bind:this={emojiContainer} />
</div> </div>
<style lang="scss"> <style lang="scss">
.emote-menu-container { .emote-menu-container {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.emote-menu { .emote-menu {
pointer-events: all; pointer-events: all;
} }
</style> </style>

View file

@ -1,22 +1,22 @@
<script lang="typescript"> <script lang="typescript">
import type {Game} from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene"; import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
import { import {
audioConstraintStore, audioConstraintStore,
cameraListStore, cameraListStore,
localStreamStore, localStreamStore,
microphoneListStore, microphoneListStore,
videoConstraintStore videoConstraintStore,
} from "../../Stores/MediaStore"; } from "../../Stores/MediaStore";
import {onDestroy} from "svelte"; import { onDestroy } from "svelte";
import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte"; import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte";
import cinemaCloseImg from "../images/cinema-close.svg"; import cinemaCloseImg from "../images/cinema-close.svg";
import cinemaImg from "../images/cinema.svg"; import cinemaImg from "../images/cinema.svg";
import microphoneImg from "../images/microphone.svg"; import microphoneImg from "../images/microphone.svg";
export let game: Game; export let game: Game;
let selectedCamera : string|undefined = undefined; let selectedCamera: string | undefined = undefined;
let selectedMicrophone : string|undefined = undefined; let selectedMicrophone: string | undefined = undefined;
const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene; const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene;
@ -29,16 +29,16 @@
return { return {
update(newStream: MediaStream) { update(newStream: MediaStream) {
if (node.srcObject != newStream) { if (node.srcObject != newStream) {
node.srcObject = newStream node.srcObject = newStream;
} }
} },
} };
} }
let stream: MediaStream | null; let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => { const unsubscribe = localStreamStore.subscribe((value) => {
if (value.type === 'success') { if (value.type === "success") {
stream = value.stream; stream = value.stream;
if (stream !== null) { if (stream !== null) {
@ -62,7 +62,7 @@
function normalizeDeviceName(label: string): string { function normalizeDeviceName(label: string): string {
// remove IDs (that can appear in Chrome, like: "HD Pro Webcam (4df7:4eda)" // remove IDs (that can appear in Chrome, like: "HD Pro Webcam (4df7:4eda)"
return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, '').trim(); return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, "").trim();
} }
function selectCamera() { function selectCamera() {
@ -72,28 +72,27 @@
function selectMicrophone() { function selectMicrophone() {
audioConstraintStore.setDeviceId(selectedMicrophone); audioConstraintStore.setDeviceId(selectedMicrophone);
} }
</script> </script>
<form class="enableCameraScene" on:submit|preventDefault={submit}> <form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="text-center"> <section class="text-center">
<h2>Turn on your camera and microphone</h2> <h2>Turn on your camera and microphone</h2>
</section> </section>
{#if $localStreamStore.type === 'success' && $localStreamStore.stream} {#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video> <video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline />
{:else } {:else}
<div class="webrtcsetup"> <div class="webrtcsetup">
<img class="background-img" src={cinemaCloseImg} alt=""> <img class="background-img" src={cinemaCloseImg} alt="" />
</div> </div>
{/if} {/if}
<HorizontalSoundMeterWidget stream={stream}></HorizontalSoundMeterWidget> <HorizontalSoundMeterWidget {stream} />
<section class="selectWebcamForm"> <section class="selectWebcamForm">
{#if $cameraListStore.length > 1}
{#if $cameraListStore.length > 1 }
<div class="control-group"> <div class="control-group">
<img src={cinemaImg} alt="Camera" /> <img src={cinemaImg} alt="Camera" />
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<!-- svelte-ignore a11y-no-onchange -->
<select bind:value={selectedCamera} on:change={selectCamera}> <select bind:value={selectedCamera} on:change={selectCamera}>
{#each $cameraListStore as camera} {#each $cameraListStore as camera}
<option value={camera.deviceId}> <option value={camera.deviceId}>
@ -105,10 +104,11 @@
</div> </div>
{/if} {/if}
{#if $microphoneListStore.length > 1 } {#if $microphoneListStore.length > 1}
<div class="control-group"> <div class="control-group">
<img src={microphoneImg} alt="Microphone" /> <img src={microphoneImg} alt="Microphone" />
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<!-- svelte-ignore a11y-no-onchange -->
<select bind:value={selectedMicrophone} on:change={selectMicrophone}> <select bind:value={selectedMicrophone} on:change={selectMicrophone}>
{#each $microphoneListStore as microphone} {#each $microphoneListStore as microphone}
<option value={microphone.deviceId}> <option value={microphone.deviceId}>
@ -119,111 +119,109 @@
</div> </div>
</div> </div>
{/if} {/if}
</section> </section>
<section class="action"> <section class="action">
<button type="submit" class="nes-btn is-primary letsgo" >Let's go!</button> <button type="submit" class="nes-btn is-primary letsgo">Let's go!</button>
</section> </section>
</form> </form>
<style lang="scss"> <style lang="scss">
.enableCameraScene { .enableCameraScene {
pointer-events: auto; pointer-events: auto;
margin: 20px auto 0; margin: 20px auto 0;
color: #ebeeee; color: #ebeeee;
section.selectWebcamForm { section.selectWebcamForm {
margin-top: 3vh; margin-top: 3vh;
margin-bottom: 3vh; margin-bottom: 3vh;
min-height: 10vh; min-height: 10vh;
width: 50vw; width: 50vw;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
select { select {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
margin-top: 1vh; margin-top: 1vh;
margin-bottom: 1vh; margin-bottom: 1vh;
}
option {
font-family: "Press Start 2P";
}
} }
option { section.action {
text-align: center;
margin: 0;
width: 100%;
}
h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
} margin: 1px;
} }
section.action{ section.text-center {
text-align: center; text-align: center;
margin: 0;
width: 100%;
}
h2{
font-family: "Press Start 2P";
margin: 1px;
}
section.text-center{
text-align: center;
}
button.letsgo {
font-size: 200%;
}
.control-group {
display: flex;
flex-direction: row;
max-height: 60px;
margin-top: 10px;
img {
width: 30px;
margin-right: 10px;
} }
}
.webrtcsetup{ button.letsgo {
margin-top: 2vh; font-size: 200%;
margin-left: auto;
margin-right: auto;
height: 28.125vw;
width: 50vw;
border: white 6px solid;
display: flex;
align-items: center;
justify-content: center;
img.background-img {
width: 40%;
} }
}
.myCamVideoSetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
max-height: 50vh;
width: 50vw;
border: white 6px solid;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
display: flex; .control-group {
align-items: center; display: flex;
justify-content: center; flex-direction: row;
} max-height: 60px;
margin-top: 10px;
img {
width: 30px;
margin-right: 10px;
}
}
.webrtcsetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
height: 28.125vw;
width: 50vw;
border: white 6px solid;
display: flex;
align-items: center;
justify-content: center;
img.background-img {
width: 40%;
}
}
.myCamVideoSetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
max-height: 50vh;
width: 50vw;
border: white 6px solid;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
display: flex;
align-items: center;
justify-content: center;
}
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
.enableCameraScene h2 { .enableCameraScene h2 {
font-size: 80%; font-size: 80%;
} }
.enableCameraScene .control-group .nes-select { .enableCameraScene .control-group .nes-select {
font-size: 80%; font-size: 80%;
} }
.enableCameraScene button.letsgo { .enableCameraScene button.letsgo {
font-size: 160%; font-size: 160%;
} }
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<script lang="typescript"> <script lang="typescript">
import { AudioContext } from 'standardized-audio-context'; import { AudioContext } from "standardized-audio-context";
import {SoundMeter} from "../../Phaser/Components/SoundMeter"; import { SoundMeter } from "../../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte"; import { onDestroy } from "svelte";
export let stream: MediaStream | null; export let stream: MediaStream | null;
let volume = 0; let volume = 0;
@ -11,6 +11,7 @@
let timeout: ReturnType<typeof setTimeout>; let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter(); const soundMeter = new SoundMeter();
let display = false; let display = false;
let error = false;
$: { $: {
if (stream && stream.getAudioTracks().length > 0) { if (stream && stream.getAudioTracks().length > 0) {
@ -19,17 +20,19 @@
if (timeout) { if (timeout) {
clearInterval(timeout); clearInterval(timeout);
error = false;
} }
timeout = setInterval(() => { timeout = setInterval(() => {
try{ try {
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0)); volume = parseInt(((soundMeter.getVolume() / 100) * NB_BARS).toFixed(0));
//console.log(volume); } catch (err) {
}catch(err){ if (!error) {
console.error(err);
error = true;
}
} }
}, 100); }, 100);
} else { } else {
display = false; display = false;
} }
@ -40,11 +43,10 @@
if (timeout) { if (timeout) {
clearInterval(timeout); clearInterval(timeout);
} }
}) });
function color(i: number, volume: number) { function color(i: number, volume: number) {
const red = 255 * i / NB_BARS; const red = (255 * i) / NB_BARS;
const green = 255 * (1 - i / NB_BARS); const green = 255 * (1 - i / NB_BARS);
let alpha = 1; let alpha = 1;
@ -52,31 +54,29 @@
alpha = 0.5; alpha = 0.5;
} }
return 'background-color:rgba('+red+', '+green+', 0, '+alpha+')'; return "background-color:rgba(" + red + ", " + green + ", 0, " + alpha + ")";
} }
</script> </script>
<div class="horizontal-sound-meter" class:active={display}> <div class="horizontal-sound-meter" class:active={display}>
{#each [...Array(NB_BARS).keys()] as i (i)} {#each [...Array(NB_BARS).keys()] as i (i)}
<div style={color(i, volume)}></div> <div style={color(i, volume)} />
{/each} {/each}
</div> </div>
<style lang="scss"> <style lang="scss">
.horizontal-sound-meter { .horizontal-sound-meter {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 50%; width: 50%;
height: 30px; height: 30px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: 1vh; margin-top: 1vh;
} }
.horizontal-sound-meter div { .horizontal-sound-meter div {
margin-left: 5px; margin-left: 5px;
flex-grow: 1; flex-grow: 1;
} }
</style> </style>

View file

@ -1,9 +1,9 @@
<script lang="typescript"> <script lang="typescript">
import { fly } from 'svelte/transition'; import { fly } from "svelte/transition";
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore"; import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import firefoxImg from "./images/help-setting-camera-permission-firefox.png"; import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png"; import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import {getNavigatorType, isAndroid as isAndroidFct, NavigatorType} from "../../WebRtc/DeviceUtils"; import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
let isAndroid = isAndroidFct(); let isAndroid = isAndroidFct();
let isFirefox = getNavigatorType() === NavigatorType.firefox; let isFirefox = getNavigatorType() === NavigatorType.firefox;
@ -16,30 +16,37 @@
function close() { function close() {
helpCameraSettingsVisibleStore.set(false); helpCameraSettingsVisibleStore.set(false);
} }
</script> </script>
<form class="helpCameraSettings nes-container" on:submit|preventDefault={close} transition:fly="{{ y: -900, duration: 500 }}"> <form
class="helpCameraSettings nes-container"
on:submit|preventDefault={close}
transition:fly={{ y: -900, duration: 500 }}
>
<section> <section>
<h2>Camera / Microphone access needed</h2> <h2>Camera / Microphone access needed</h2>
<p class="err">Permission denied</p> <p class="err">Permission denied</p>
<p>You must allow camera and microphone access in your browser.</p> <p>You must allow camera and microphone access in your browser.</p>
<p> <p>
{#if isFirefox } {#if isFirefox}
<p class="err">Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.</p> <p class="err">
Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the
authorization.
</p>
<img src={firefoxImg} alt="" /> <img src={firefoxImg} alt="" />
{:else if isChrome && !isAndroid } {:else if isChrome && !isAndroid}
<img src={chromeImg} alt="" /> <img src={chromeImg} alt="" />
{/if} {/if}
</p> </p>
</section> </section>
<section> <section>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button> <button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}>Continue without webcam</button> <button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}
>Continue without webcam</button
>
</section> </section>
</form> </form>
<style lang="scss"> <style lang="scss">
.helpCameraSettings { .helpCameraSettings {
pointer-events: auto; pointer-events: auto;
@ -53,13 +60,13 @@
text-align: center; text-align: center;
h2 { h2 {
font-family: 'Press Start 2P'; font-family: "Press Start 2P";
} }
section { section {
p { p {
margin: 15px; margin: 15px;
font-family: 'Press Start 2P'; font-family: "Press Start 2P";
& .err { & .err {
color: #ff0000; color: #ff0000;

View file

@ -6,7 +6,6 @@
} }
</script> </script>
<div class="layout-manager-list"> <div class="layout-manager-list">
{#each $layoutManagerActionStore as action} {#each $layoutManagerActionStore as action}
<div class="nes-container is-rounded {action.type}" on:click={() => onClick(action.callback)}> <div class="nes-container is-rounded {action.type}" on:click={() => onClick(action.callback)}>
@ -15,43 +14,48 @@
{/each} {/each}
</div> </div>
<style lang="scss"> <style lang="scss">
div.layout-manager-list { div.layout-manager-list {
pointer-events: auto; pointer-events: auto;
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 40px; bottom: 40px;
margin: 0 auto; margin: 0 auto;
padding: 0; padding: 0;
width: clamp(200px, 20vw, 20vw); width: clamp(200px, 20vw, 20vw);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: moveMessage .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.nes-container.is-rounded {
padding: 8px 4px;
text-align: center;
font-family: Lato;
color: whitesmoke;
background-color: rgb(0,0,0,0.5);
&.warning {
background-color: #ff9800eb;
color: #000;
} }
}
@keyframes moveMessage { div.nes-container.is-rounded {
0% {bottom: 40px;} padding: 8px 4px;
50% {bottom: 30px;} text-align: center;
100% {bottom: 40px;}
} font-family: Lato;
color: whitesmoke;
background-color: rgb(0, 0, 0, 0.5);
&.warning {
background-color: #ff9800eb;
color: #000;
}
}
@keyframes moveMessage {
0% {
bottom: 40px;
}
50% {
bottom: 30px;
}
100% {
bottom: 40px;
}
}
</style> </style>

View file

@ -1,22 +1,22 @@
<script lang="typescript"> <script lang="typescript">
import type {Game} from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene"; import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
import {DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH} from "../../Enum/EnvironmentVariable"; import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png"; import logoImg from "../images/logo.png";
import {gameManager} from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
export let game: Game; export let game: Game;
const loginScene = game.scene.getScene(LoginSceneName) as LoginScene; const loginScene = game.scene.getScene(LoginSceneName) as LoginScene;
let name = gameManager.getPlayerName() || ''; let name = gameManager.getPlayerName() || "";
let startValidating = false; let startValidating = false;
function submit() { function submit() {
startValidating = true; startValidating = true;
let finalName = name.trim(); let finalName = name.trim();
if (finalName !== '') { if (finalName !== "") {
loginScene.login(finalName); loginScene.login(finalName);
} }
} }
@ -29,17 +29,34 @@
<section class="text-center"> <section class="text-center">
<h2>Enter your name</h2> <h2>Enter your name</h2>
</section> </section>
<input type="text" name="loginSceneName" class="nes-input is-dark" autofocus maxlength={MAX_USERNAME_LENGTH} bind:value={name} on:keypress={() => {startValidating = true}} class:is-error={name.trim() === '' && startValidating} /> <!-- svelte-ignore a11y-autofocus -->
<input
type="text"
name="loginSceneName"
class="nes-input is-dark"
autofocus
maxlength={MAX_USERNAME_LENGTH}
bind:value={name}
on:keypress={() => {
startValidating = true;
}}
class:is-error={name.trim() === "" && startValidating}
/>
<section class="error-section"> <section class="error-section">
{#if name.trim() === '' && startValidating } {#if name.trim() === "" && startValidating}
<p class="err">The name is empty</p> <p class="err">The name is empty</p>
{/if} {/if}
</section> </section>
{#if DISPLAY_TERMS_OF_USE} {#if DISPLAY_TERMS_OF_USE}
<section class="terms-and-conditions"> <section class="terms-and-conditions">
<p>By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.</p> <p>
</section> By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank"
>terms of use</a
>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and
<a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.
</p>
</section>
{/if} {/if}
<section class="action"> <section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button> <button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
@ -47,76 +64,75 @@
</form> </form>
<style lang="scss"> <style lang="scss">
.loginScene { .loginScene {
pointer-events: auto; pointer-events: auto;
margin: 20px auto 0; margin: 20px auto 0;
width: 90%; width: 90%;
color: #ebeeee;
display: flex;
flex-flow: column wrap;
align-items: center;
input {
text-align: center;
font-family: "Press Start 2P";
max-width: 400px;
}
.terms-and-conditions {
max-width: 400px;
}
p.err {
color: #ce372b;
text-align: center;
}
section {
margin: 10px;
&.error-section {
min-height: 2rem;
margin: 0;
p {
margin: 0;
}
}
&.action {
text-align: center;
margin-top: 20px;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
a {
text-decoration: underline;
color: #ebeeee; color: #ebeeee;
}
a:hover { display: flex;
font-weight: 700; flex-flow: column wrap;
} align-items: center;
p { input {
text-align: left; text-align: center;
margin: 10px 10px; font-family: "Press Start 2P";
} max-width: 400px;
}
img { .terms-and-conditions {
width: 100%; max-width: 400px;
margin: 20px 0; }
}
p.err {
color: #ce372b;
text-align: center;
}
section {
margin: 10px;
&.error-section {
min-height: 2rem;
margin: 0;
p {
margin: 0;
}
}
&.action {
text-align: center;
margin-top: 20px;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
a {
text-decoration: underline;
color: #ebeeee;
}
a:hover {
font-weight: 700;
}
p {
text-align: left;
margin: 10px 10px;
}
img {
width: 100%;
margin: 20px 0;
}
}
} }
}
</style> </style>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import {onMount} from "svelte"; import { onMount } from "svelte";
let gameScene = gameManager.getCurrentGameScene(); let gameScene = gameManager.getCurrentGameScene();
@ -14,29 +14,33 @@
onMount(() => { onMount(() => {
if (gameScene.mapFile.properties !== undefined) { if (gameScene.mapFile.properties !== undefined) {
const propertyName = gameScene.mapFile.properties.find((property) => property.name === 'mapName') const propertyName = gameScene.mapFile.properties.find((property) => property.name === "mapName");
if ( propertyName !== undefined && typeof propertyName.value === 'string') { if (propertyName !== undefined && typeof propertyName.value === "string") {
mapName = propertyName.value; mapName = propertyName.value;
} }
const propertyDescription = gameScene.mapFile.properties.find((property) => property.name === 'mapDescription') const propertyDescription = gameScene.mapFile.properties.find(
if (propertyDescription !== undefined && typeof propertyDescription.value === 'string') { (property) => property.name === "mapDescription"
);
if (propertyDescription !== undefined && typeof propertyDescription.value === "string") {
mapDescription = propertyDescription.value; mapDescription = propertyDescription.value;
} }
const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === 'mapCopyright') const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === "mapCopyright");
if (propertyCopyright !== undefined && typeof propertyCopyright.value === 'string') { if (propertyCopyright !== undefined && typeof propertyCopyright.value === "string") {
mapCopyright = propertyCopyright.value; mapCopyright = propertyCopyright.value;
} }
} }
for (const tileset of gameScene.mapFile.tilesets) { for (const tileset of gameScene.mapFile.tilesets) {
if (tileset.properties !== undefined) { if (tileset.properties !== undefined) {
const propertyTilesetCopyright = tileset.properties.find((property) => property.name === 'tilesetCopyright') const propertyTilesetCopyright = tileset.properties.find(
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === 'string') { (property) => property.name === "tilesetCopyright"
);
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") {
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
} }
} }
} }
}) });
</script> </script>
<div class="about-room-main"> <div class="about-room-main">
@ -44,51 +48,58 @@
<section class="container-overflow"> <section class="container-overflow">
<h3>{mapName}</h3> <h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p> <p class="string-HTML">{mapDescription}</p>
<h3 class="nes-pointer hoverable" on:click={() => expandedMapCopyright = !expandedMapCopyright}>Copyrights of the map</h3> <h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
<p class="string-HTML" hidden="{!expandedMapCopyright}">{mapCopyright}</p> Copyrights of the map
<h3 class="nes-pointer hoverable" on:click={() => expandedTilesetCopyright = !expandedTilesetCopyright}>Copyrights of the tilesets</h3> </h3>
<section hidden="{!expandedTilesetCopyright}"> <p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}>
Copyrights of the tilesets
</h3>
<section hidden={!expandedTilesetCopyright}>
{#each tilesetCopyright as copyright} {#each tilesetCopyright as copyright}
<p class="string-HTML">{copyright}</p> <p class="string-HTML">{copyright}</p>
{:else} {:else}
<p>The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those tilesets have no license.</p> <p>
The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those
tilesets have no license.
</p>
{/each} {/each}
</section> </section>
</section> </section>
</div> </div>
<style lang="scss"> <style lang="scss">
.string-HTML{ .string-HTML {
white-space: pre-line; white-space: pre-line;
}
div.about-room-main {
height: calc(100% - 56px);
h2, h3 {
width: 100%;
text-align: center;
} }
h3.hoverable:hover {
background-color: #3c3e40;
border-radius: 32px;
}
section.container-overflow {
height: calc(100% - 220px);
margin: 0;
padding: 0;
overflow-y: auto;
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.about-room-main { div.about-room-main {
section.container-overflow { height: calc(100% - 56px);
height: calc(100% - 120px);
} h2,
h3 {
width: 100%;
text-align: center;
}
h3.hoverable:hover {
background-color: #3c3e40;
border-radius: 32px;
}
section.container-overflow {
height: calc(100% - 220px);
margin: 0;
padding: 0;
overflow-y: auto;
}
} }
}
</style> @media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.about-room-main {
section.container-overflow {
height: calc(100% - 120px);
}
}
}
</style>

View file

@ -26,31 +26,31 @@
const selectedFile = inputAudio.files ? inputAudio.files[0] : null; const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) { if (!selectedFile) {
errorFile = true; errorFile = true;
throw 'no file selected'; throw "no file selected";
} }
const fd = new FormData(); const fd = new FormData();
fd.append('file', selectedFile); fd.append("file", selectedFile);
const res = await gameScene.connection?.uploadAudio(fd); const res = await gameScene.connection?.uploadAudio(fd);
const audioGlobalMessage: PlayGlobalMessageInterface = { const audioGlobalMessage: PlayGlobalMessageInterface = {
content: (res as { path: string }).path, content: (res as { path: string }).path,
type: AUDIO_TYPE, type: AUDIO_TYPE,
broadcastToWorld: broadcast broadcastToWorld: broadcast,
} };
inputAudio.value = ''; inputAudio.value = "";
gameScene.connection?.emitGlobalMessage(audioGlobalMessage); gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
} },
} };
function inputAudioFile(event: Event) { function inputAudioFile(event: Event) {
const eventTarget : EventTargetFiles = (event.target as EventTargetFiles); const eventTarget: EventTargetFiles = event.target as EventTargetFiles;
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){ if (!eventTarget || !eventTarget.files || eventTarget.files.length === 0) {
return; return;
} }
const file = eventTarget.files[0]; const file = eventTarget.files[0];
if(!file) { if (!file) {
return; return;
} }
@ -61,52 +61,65 @@
function getFileSize(number: number) { function getFileSize(number: number) {
if (number < 1024) { if (number < 1024) {
return number + 'bytes'; return number + "bytes";
} else if (number >= 1024 && number < 1048576) { } else if (number >= 1024 && number < 1048576) {
return (number / 1024).toFixed(1) + 'KB'; return (number / 1024).toFixed(1) + "KB";
} else if (number >= 1048576) { } else if (number >= 1048576) {
return (number / 1048576).toFixed(1) + 'MB'; return (number / 1048576).toFixed(1) + "MB";
} else { } else {
return ''; return "";
} }
} }
</script> </script>
<section class="section-input-send-audio"> <section class="section-input-send-audio">
<img class="nes-pointer" src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileInput.click();}}> <img
class="nes-pointer"
src={uploadFile}
alt="Upload a file"
on:click|preventDefault={() => {
fileInput.click();
}}
/>
{#if fileName !== undefined} {#if fileName !== undefined}
<p>{fileName} : {fileSize}</p> <p>{fileName} : {fileSize}</p>
{/if} {/if}
{#if errorFile} {#if errorFile}
<p class="err">No file selected. You need to upload a file before sending it.</p> <p class="err">No file selected. You need to upload a file before sending it.</p>
{/if} {/if}
<input type="file" id="input-send-audio" bind:this={fileInput} on:change={(e) => {inputAudioFile(e)}}> <input
type="file"
id="input-send-audio"
bind:this={fileInput}
on:change={(e) => {
inputAudioFile(e);
}}
/>
</section> </section>
<style lang="scss"> <style lang="scss">
section.section-input-send-audio { section.section-input-send-audio {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
text-align: center; text-align: center;
img { img {
flex: 1 1 auto; flex: 1 1 auto;
max-height: 80%; max-height: 80%;
margin-bottom: 20px; margin-bottom: 20px;
}
p {
margin-bottom: 5px;
color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}
} }
p { </style>
margin-bottom: 5px;
color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}
}
</style>

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
function goToGettingStarted() { function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started"; const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank"); window.open(sparkHost, "_blank");
@ -10,7 +9,7 @@
window.open(sparkHost, "_blank"); window.open(sparkHost, "_blank");
} }
import {contactPageStore} from "../../Stores/MenuStore"; import { contactPageStore } from "../../Stores/MenuStore";
</script> </script>
<div class="create-map-main"> <div class="create-map-main">
@ -18,8 +17,8 @@
<section> <section>
<h3>Getting started</h3> <h3>Getting started</h3>
<p> <p>
WorkAdventure allows you to create an online space to communicate spontaneously with others. WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all
And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team. starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
</p> </p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button> <button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
</section> </section>
@ -30,35 +29,37 @@
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button> <button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
</section> </section>
<iframe title="contact" <iframe
src="{$contactPageStore}" title="contact"
allow="clipboard-read; clipboard-write self {$contactPageStore}" src={$contactPageStore}
allowfullscreen></iframe> allow="clipboard-read; clipboard-write self {$contactPageStore}"
allowfullscreen
/>
</section> </section>
</div> </div>
<style lang="scss"> <style lang="scss">
div.create-map-main { div.create-map-main {
height: calc(100% - 56px); height: calc(100% - 56px);
text-align: center; text-align: center;
section { section {
margin-bottom: 50px; margin-bottom: 50px;
} }
section.container-overflow { section.container-overflow {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: auto; overflow: auto;
} }
} }
iframe { iframe {
border: none; border: none;
height: calc(100% - 56px); height: calc(100% - 56px);
width: 100%; width: 100%;
margin: 0; margin: 0;
} }
</style> </style>

View file

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

View file

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

View file

@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
function copyLink() { function copyLink() {
const input: HTMLInputElement = document.getElementById('input-share-link') as HTMLInputElement; const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
input.focus(); input.focus();
input.select(); input.select();
document.execCommand('copy'); document.execCommand("copy");
} }
async function shareLink() { async function shareLink() {
const shareData = {url: location.toString()}; const shareData = { url: location.toString() };
try { try {
await navigator.share(shareData); await navigator.share(shareData);
} catch (err) { } catch (err) {
console.error('Error: ' + err); console.error("Error: " + err);
copyLink(); copyLink();
} }
} }
@ -22,12 +22,12 @@
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> <section class="share-url not-mobile">
<h3>Share the link of the room !</h3> <h3>Share the link of the room !</h3>
<input type="text" readonly id="input-share-link" value={location.toString()}> <input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button> <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
</section> </section>
<section class="is-mobile"> <section class="is-mobile">
<h3>Share the link of the room !</h3> <h3>Share the link of the room !</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()}> <input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button> <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section> </section>
</section> </section>
@ -35,41 +35,41 @@
<style lang="scss"> <style lang="scss">
div.guest-main { div.guest-main {
height: calc(100% - 56px); height: calc(100% - 56px);
text-align: center; text-align: center;
section { section {
margin-bottom: 50px; margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
section.is-mobile {
display: none;
}
}
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
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: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
section.is-mobile {
display: none;
} }
}
} }
</style>
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow {
height: calc(100% - 120px);
}
}
}
</style>

View file

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

View file

@ -1,47 +1,65 @@
<script lang="typescript"> <script lang="typescript">
import logoWA from "../images/logo-WA-pixel.png" import logoTalk from "../images/logo-message-pixel.png";
import logoTalk from "../images/logo-message-pixel.png" import logoWA from "../images/logo-WA-pixel.png";
import {menuVisiblilityStore} from "../../Stores/MenuStore"; import { menuVisiblilityStore } from "../../Stores/MenuStore";
import {chatVisibilityStore} from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import {get} from "svelte/store"; import { get } from "svelte/store";
function showMenu(){ function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore)) menuVisiblilityStore.set(!get(menuVisiblilityStore));
} }
function showChat(){ function showChat() {
chatVisibilityStore.set(true); chatVisibilityStore.set(true);
} }
</script> </script>
<svelte:window/> <svelte:window />
<main class="menuIcon"> <main class="menuIcon">
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu}> <img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat}> <img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
</main> </main>
<style lang="scss"> <style lang="scss">
.menuIcon {
display: inline-grid;
z-index: 90;
position: relative;
margin: 25px;
img {
pointer-events: auto;
width: 60px;
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: 3px; display: inline-grid;
img { z-index: 90;
width: 50px; position: relative;
} margin: 25px;
img {
pointer-events: auto;
width: 60px;
padding-top: 0;
margin: 3px;
image-rendering: pixelated;
}
}
.menuIcon img:hover {
transform: scale(1.2);
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.menuIcon {
display: inline-grid;
z-index: 90;
position: relative;
margin: 25px;
img {
pointer-events: auto;
width: 60px;
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 {
margin: 3px;
img {
width: 50px;
}
}
}
} }
}
</style> </style>

View file

@ -1,60 +1,60 @@
<script lang="typescript"> <script lang="typescript">
import {gameManager} from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore"; import { menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected } from "../../Stores/MenuStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore"; import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene"; import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore"; import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import {connectionManager} from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import {PROFILE_URL} from "../../Enum/EnvironmentVariable"; import { PROFILE_URL } from "../../Enum/EnvironmentVariable";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
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 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 btnProfileSubMenuWoka from "../images/btn-menu-profile-woka.svg"; import Woka from "../Woka/Woka.svelte";
import Companion from "../Companion/Companion.svelte";
function disableMenuStores() {
function disableMenuStores(){
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
menuIconVisiblilityStore.set(false); menuIconVisiblilityStore.set(false);
} }
function openEditCompanionScene(){ function openEditCompanionScene() {
disableMenuStores(); disableMenuStores();
selectCompanionSceneVisibleStore.set(true); selectCompanionSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCompanionSceneName,new SelectCompanionScene()); gameManager.leaveGame(SelectCompanionSceneName, new SelectCompanionScene());
} }
function openEditNameScene(){ function openEditNameScene() {
disableMenuStores(); disableMenuStores();
loginSceneVisibleStore.set(true); loginSceneVisibleStore.set(true);
gameManager.leaveGame(LoginSceneName,new LoginScene()); gameManager.leaveGame(LoginSceneName, new LoginScene());
} }
function openEditSkinScene(){ function openEditSkinScene() {
disableMenuStores(); disableMenuStores();
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
} }
function logOut(){ function logOut() {
disableMenuStores(); disableMenuStores();
loginSceneVisibleStore.set(true); loginSceneVisibleStore.set(true);
connectionManager.logout(); connectionManager.logout();
} }
function getProfileUrl(){ function getProfileUrl() {
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`; return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
} }
function openEnableCameraScene(){ function openEnableCameraScene() {
disableMenuStores(); disableMenuStores();
enableCameraSceneVisibilityStore.showEnableCameraScene(); enableCameraSceneVisibilityStore.showEnableCameraScene();
gameManager.leaveGame(EnableCameraSceneName,new EnableCameraScene()); gameManager.leaveGame(EnableCameraSceneName, new EnableCameraScene());
} }
</script> </script>
@ -62,19 +62,19 @@
<div class="submenu"> <div class="submenu">
<section> <section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
<img src={btnProfileSubMenuIdentity} alt="Edit your name"> <img src={btnProfileSubMenuIdentity} alt="Edit your name" />
<span class="btn-hover">Edit your name</span> <span class="btn-hover">Edit your name</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<img src={btnProfileSubMenuWoka} alt="Edit your WOKA"> <Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
<span class="btn-hover">Edit your WOKA</span> <span class="btn-hover">Edit your WOKA</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<img src={btnProfileSubMenuCompanion} alt="Edit your companion"> <Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
<span class="btn-hover">Edit your companion</span> <span class="btn-hover">Edit your companion</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
<img src={btnProfileSubMenuCamera} alt="Edit your camera"> <img src={btnProfileSubMenuCamera} alt="Edit your camera" />
<span class="btn-hover">Edit your camera</span> <span class="btn-hover">Edit your camera</span>
</button> </button>
</section> </section>
@ -84,7 +84,7 @@
{#if $userIsConnected} {#if $userIsConnected}
<section> <section>
{#if PROFILE_URL != undefined} {#if PROFILE_URL != undefined}
<iframe title="profile" src="{getProfileUrl()}"></iframe> <iframe title="profile" src={getProfileUrl()} />
{/if} {/if}
</section> </section>
<section> <section>
@ -99,68 +99,68 @@
</div> </div>
<style lang="scss"> <style lang="scss">
div.customize-main{ div.customize-main {
width: 100%;
display: inline-flex;
div.submenu{
height: 100%;
width: 50px;
button {
transition: all .5s ease;
text-align: left;
white-space: nowrap;
margin-bottom: 10px;
max-height: 44px;
img {
height: 26px;
width: 26px;
cursor: pointer;
}
span.btn-hover{
display: none;
font-family: "Press Start 2P";
}
&:hover{
width: auto;
span.btn-hover {
display: initial;
}
}
}
}
div.content {
width: 100%; width: 100%;
section { display: inline-flex;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
iframe { div.submenu {
width: 100%; height: 100%;
height: 50vh; width: 50px;
border: none;
}
button { button {
height: 50px; transition: all 0.5s ease;
width: 250px; text-align: left;
} white-space: nowrap;
margin-bottom: 10px;
max-height: 44px;
img {
height: 26px;
width: 26px;
cursor: pointer;
}
span.btn-hover {
display: none;
font-family: "Press Start 2P";
}
&:hover {
width: auto;
span.btn-hover {
display: initial;
}
}
}
}
div.content {
width: 100%;
section {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
iframe {
width: 100%;
height: 50vh;
border: none;
}
button {
height: 50px;
width: 250px;
}
}
} }
}
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
div.customize-main.content section button { div.customize-main.content section button {
width: 130px; width: 130px;
} }
} }
</style> </style>

View file

@ -1,62 +1,67 @@
<script lang="typescript"> <script lang="typescript">
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 { isMobile } from "../../Enum/EnvironmentVariable";
import {menuVisiblilityStore} from "../../Stores/MenuStore"; import { menuVisiblilityStore } from "../../Stores/MenuStore";
let fullscreen : boolean = localUserStore.getFullscreen(); let fullscreen: boolean = localUserStore.getFullscreen();
let notification : boolean = localUserStore.getNotification() === 'granted'; let notification: boolean = localUserStore.getNotification() === "granted";
let valueGame : number = localUserStore.getGameQualityValue(); let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger();
let valueVideo : number = localUserStore.getVideoQualityValue(); let valueGame: number = localUserStore.getGameQualityValue();
let previewValueGame = valueGame; let valueVideo: number = localUserStore.getVideoQualityValue();
let previewValueVideo = valueVideo; let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
function saveSetting(){ function saveSetting() {
if (valueGame !== previewValueGame) { if (valueGame !== previewValueGame) {
previewValueGame = valueGame; previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame); localUserStore.setGameQualityValue(valueGame);
window.location.reload(); window.location.reload();
}
if (valueVideo !== previewValueVideo) {
previewValueVideo = valueVideo;
videoConstraintStore.setFrameRate(valueVideo);
}
closeMenu();
}
function changeFullscreen() {
const body = HtmlUtils.querySelectorOrFail('body');
if (body) {
if (document.fullscreenElement !== null && !fullscreen) {
document.exitFullscreen()
} else {
body.requestFullscreen();
} }
localUserStore.setFullscreen(fullscreen);
}
}
function changeNotification() { if (valueVideo !== previewValueVideo) {
if (Notification.permission === 'granted') { previewValueVideo = valueVideo;
localUserStore.setNotification(notification ? 'granted' : 'denied'); videoConstraintStore.setFrameRate(valueVideo);
} else { }
Notification.requestPermission().then((response) => {
if (response === 'granted') { closeMenu();
localUserStore.setNotification(notification ? 'granted' : 'denied'); }
function changeFullscreen() {
const body = HtmlUtils.querySelectorOrFail("body");
if (body) {
if (document.fullscreenElement !== null && !fullscreen) {
document.exitFullscreen();
} else { } else {
localUserStore.setNotification('denied'); body.requestFullscreen();
notification = false;
} }
}) localUserStore.setFullscreen(fullscreen);
}
} }
}
function closeMenu() { function changeNotification() {
menuVisiblilityStore.set(false); if (Notification.permission === "granted") {
} localUserStore.setNotification(notification ? "granted" : "denied");
} else {
Notification.requestPermission().then((response) => {
if (response === "granted") {
localUserStore.setNotification(notification ? "granted" : "denied");
} else {
localUserStore.setNotification("denied");
notification = false;
}
});
}
}
function changeForceCowebsiteTrigger() {
localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger);
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
</script> </script>
<div class="settings-main" on:submit|preventDefault={saveSetting}> <div class="settings-main" on:submit|preventDefault={saveSetting}>
@ -64,10 +69,12 @@ function closeMenu() {
<h3>Game quality</h3> <h3>Game quality</h3>
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueGame}> <select bind:value={valueGame}>
<option value="{120}">{isMobile() ? 'High (120 fps)' : 'High video quality (120 fps)'}</option> <option value={120}>{isMobile() ? "High (120 fps)" : "High video quality (120 fps)"}</option>
<option value="{60}">{isMobile() ? 'Medium (60 fps)' : 'Medium video quality (60 fps, recommended)'}</option> <option value={60}
<option value="{40}">{isMobile() ? 'Minimum (40 fps)' : 'Minimum video quality (40 fps)'}</option> >{isMobile() ? "Medium (60 fps)" : "Medium video quality (60 fps, recommended)"}</option
<option value="{20}">{isMobile() ? 'Small (20 fps)' : 'Small video quality (20 fps)'}</option> >
<option value={40}>{isMobile() ? "Minimum (40 fps)" : "Minimum video quality (40 fps)"}</option>
<option value={20}>{isMobile() ? "Small (20 fps)" : "Small video quality (20 fps)"}</option>
</select> </select>
</div> </div>
</section> </section>
@ -75,10 +82,12 @@ function closeMenu() {
<h3>Video quality</h3> <h3>Video quality</h3>
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueVideo}> <select bind:value={valueVideo}>
<option value="{30}">{isMobile() ? 'High (30 fps)' : 'High video quality (30 fps)'}</option> <option value={30}>{isMobile() ? "High (30 fps)" : "High video quality (30 fps)"}</option>
<option value="{20}">{isMobile() ? 'Medium (20 fps)' : 'Medium video quality (20 fps, recommended)'}</option> <option value={20}
<option value="{10}">{isMobile() ? 'Minimum (10 fps)' : 'Minimum video quality (10 fps)'}</option> >{isMobile() ? "Medium (20 fps)" : "Medium video quality (20 fps, recommended)"}</option
<option value="{5}">{isMobile() ? 'Small (5 fps)' : 'Small video quality (5 fps)'}</option> >
<option value={10}>{isMobile() ? "Minimum (10 fps)" : "Minimum video quality (10 fps)"}</option>
<option value={5}>{isMobile() ? "Small (5 fps)" : "Small video quality (5 fps)"}</option>
</select> </select>
</div> </div>
</section> </section>
@ -88,55 +97,74 @@ function closeMenu() {
</section> </section>
<section class="settings-section-noSaveOption"> <section class="settings-section-noSaveOption">
<label> <label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={fullscreen} on:change={changeFullscreen}/> <input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={fullscreen}
on:change={changeFullscreen}
/>
<span>Fullscreen</span> <span>Fullscreen</span>
</label> </label>
<label> <label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={notification} on:change={changeNotification}> <input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={notification}
on:change={changeNotification}
/>
<span>Notifications</span> <span>Notifications</span>
</label> </label>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={forceCowebsiteTrigger}
on:change={changeForceCowebsiteTrigger}
/>
<span>Always ask before opening websites and Jitsi Meet rooms</span>
</label>
</section> </section>
</div> </div>
<style lang="scss"> <style lang="scss">
div.settings-main {
height: calc(100% - 40px);
overflow-y: auto;
section {
width: 100%;
padding: 20px 20px 0;
margin-bottom: 20px;
text-align: center;
div.nes-select select:focus {
outline: none;
}
}
section.settings-section-save {
text-align: center;
p {
margin: 16px 0;
}
}
section.settings-section-noSaveOption {
display: flex;
align-items: center;
flex-wrap: wrap;
label {
flex: 1 1 auto;
text-align: center;
margin: 0 0 15px;
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.settings-main { div.settings-main {
section { height: calc(100% - 40px);
padding: 0; overflow-y: auto;
}
section {
width: 100%;
padding: 20px 20px 0;
margin-bottom: 20px;
text-align: center;
div.nes-select select:focus {
outline: none;
}
}
section.settings-section-save {
text-align: center;
p {
margin: 16px 0;
}
}
section.settings-section-noSaveOption {
display: flex;
align-items: center;
flex-wrap: wrap;
label {
flex: 1 1 auto;
text-align: center;
margin: 0 0 15px;
}
}
} }
}
</style> @media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.settings-main {
section {
padding: 0;
}
}
}
</style>

View file

@ -8,25 +8,25 @@
//toolbar //toolbar
const toolbarOptions = [ const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons ["bold", "italic", "underline", "strike"], // toggled buttons
['blockquote', 'code-block'], ["blockquote", "code-block"],
[{'header': 1}, {'header': 2}], // custom button values [{ header: 1 }, { header: 2 }], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}], [{ list: "ordered" }, { list: "bullet" }],
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript [{ script: "sub" }, { script: "super" }], // superscript/subscript
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent [{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{'direction': 'rtl'}], // text direction [{ direction: "rtl" }], // text direction
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown [{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}], [{ header: [1, 2, 3, 4, 5, 6, false] }],
[{'color': []}, {'background': []}], // dropdown with defaults from theme [{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{'font': []}], [{ font: [] }],
[{'align': []}], [{ align: [] }],
['clean'], // remove formatting button ["clean"], // remove formatting button
['link', 'image', 'video'] ["link", "image", "video"],
]; ];
const gameScene = gameManager.getCurrentGameScene(); const gameScene = gameManager.getCurrentGameScene();
@ -44,24 +44,24 @@
const textGlobalMessage: PlayGlobalMessageInterface = { const textGlobalMessage: PlayGlobalMessageInterface = {
type: MESSAGE_TYPE, type: MESSAGE_TYPE,
content: text, content: text,
broadcastToWorld: broadcastToWorld broadcastToWorld: broadcastToWorld,
}; };
quill.deleteText(0, quill.getLength()); quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(textGlobalMessage); gameScene.connection?.emitGlobalMessage(textGlobalMessage);
} },
} };
//Quill //Quill
onMount(async () => { onMount(async () => {
// Import quill // Import quill
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(QUILL_EDITOR, { quill = new Quill(QUILL_EDITOR, {
placeholder: 'Enter your message here...', placeholder: "Enter your message here...",
theme: 'snow', theme: "snow",
modules: { modules: {
toolbar: toolbarOptions toolbar: toolbarOptions,
}, },
}); });
menuInputFocusStore.set(true); menuInputFocusStore.set(true);
@ -69,14 +69,13 @@
onDestroy(() => { onDestroy(() => {
menuInputFocusStore.set(false); menuInputFocusStore.set(false);
}) });
</script> </script>
<section class="section-input-send-text"> <section class="section-input-send-text">
<div class="input-send-text" bind:this={QUILL_EDITOR}></div> <div class="input-send-text" bind:this={QUILL_EDITOR} />
</section> </section>
<style lang="scss"> <style lang="scss">
@import 'https://cdn.quilljs.com/1.3.7/quill.snow.css'; @import "https://cdn.quilljs.com/1.3.7/quill.snow.css";
</style> </style>

View file

@ -1,14 +1,14 @@
<script lang="typescript"> <script lang="typescript">
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 } from "svelte";
import {srcObject} from "./Video/utils"; import { srcObject } from "./Video/utils";
let stream : MediaStream|null; let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => { const unsubscribe = localStreamStore.subscribe((value) => {
if (value.type === 'success') { if (value.type === "success") {
stream = value.stream; stream = value.stream;
} else { } else {
stream = null; stream = null;
@ -17,25 +17,20 @@
onDestroy(unsubscribe); onDestroy(unsubscribe);
let isSilent: boolean; let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => { const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
isSilent = value; isSilent = value;
}); });
onDestroy(unsubscribeIsSilent); onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div>
<div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}> <div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream} {#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline></video> <video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline />
<SoundMeterWidget stream={stream}></SoundMeterWidget> <SoundMeterWidget {stream} />
{/if} {/if}
</div> </div>
<div class="is-silent" class:hide={isSilent}> <div class="is-silent" class:hide={isSilent}>Silent zone</div>
Silent zone
</div>
</div> </div>

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import type {Game} from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
export let game: Game; export let game: Game;
@ -26,62 +26,70 @@
<form class="selectCompanionScene"> <form class="selectCompanionScene">
<section class="text-center"> <section class="text-center">
<h2>Select your companion</h2> <h2>Select your companion</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> &lt; </button> <button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}> &gt; </button> &lt;
</button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}>
&gt;
</button>
</section> </section>
<section class="action"> <section class="action">
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}>No companion</button> <button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}
<button type="submit" class="selectCompanionSceneFormSubmit nes-btn is-primary" on:click|preventDefault={selectCompanion}>Continue</button> >No companion</button
>
<button
type="submit"
class="selectCompanionSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectCompanion}>Continue</button
>
</section> </section>
</form> </form>
<style lang="scss"> <style lang="scss">
form.selectCompanionScene { form.selectCompanionScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
margin: 1px; pointer-events: auto;
} color: #ebeeee;
&.text-center { section {
text-align: center; margin: 10px;
}
button.selectCharacterButton { &.action {
position: absolute; text-align: center;
top: 33vh; margin-top: 55vh;
margin: 0; }
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
}
button.selectCharacterButtonLeft {
left: 33vw;
}
button.selectCharacterButtonRight {
right: 33vw;
}
} }
button.selectCharacterButtonLeft { @media only screen and (max-width: 800px) {
left: 33vw; form.selectCompanionScene button.selectCharacterButtonLeft {
left: 5vw;
}
form.selectCompanionScene button.selectCharacterButtonRight {
right: 5vw;
}
} }
button.selectCharacterButtonRight {
right: 33vw;
}
}
@media only screen and (max-width: 800px) {
form.selectCompanionScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCompanionScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style> </style>

View file

@ -1,14 +1,15 @@
<script lang="typescript"> <script lang="typescript">
import { AudioContext } from 'standardized-audio-context'; import { AudioContext } from "standardized-audio-context";
import {SoundMeter} from "../Phaser/Components/SoundMeter"; import { SoundMeter } from "../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte"; import { onDestroy } from "svelte";
export let stream: MediaStream|null; export let stream: MediaStream | null;
let volume = 0; let volume = 0;
let timeout: ReturnType<typeof setTimeout>; let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter(); const soundMeter = new SoundMeter();
let display = false; let display = false;
let error = false;
$: { $: {
if (stream && stream.getAudioTracks().length > 0) { if (stream && stream.getAudioTracks().length > 0) {
@ -17,17 +18,19 @@
if (timeout) { if (timeout) {
clearInterval(timeout); clearInterval(timeout);
error = false;
} }
timeout = setInterval(() => { timeout = setInterval(() => {
try{ try {
volume = soundMeter.getVolume(); volume = soundMeter.getVolume();
//console.log(volume); } catch (err) {
}catch(err){ if (!error) {
console.error(err);
error = true;
}
} }
}, 100); }, 100);
} else { } else {
display = false; display = false;
} }
@ -38,14 +41,13 @@
if (timeout) { if (timeout) {
clearInterval(timeout); clearInterval(timeout);
} }
}) });
</script> </script>
<div class="sound-progress" class:active={display}> <div class="sound-progress" class:active={display}>
<span class:active={volume > 5}></span> <span class:active={volume > 5} />
<span class:active={volume > 10}></span> <span class:active={volume > 10} />
<span class:active={volume > 15}></span> <span class:active={volume > 15} />
<span class:active={volume > 40}></span> <span class:active={volume > 40} />
<span class:active={volume > 70}></span> <span class:active={volume > 70} />
</div> </div>

View file

@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore"; import { banMessageVisibleStore, banMessageContentStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import {onMount} from "svelte"; import { onMount } from "svelte";
const text = $banMessageContentStore; const text = $banMessageContentStore;
const NAME_BUTTON = 'Ok'; const NAME_BUTTON = "Ok";
let nbSeconds = 10; let nbSeconds = 10;
let nameButton = ''; let nameButton = "";
onMount(() => { onMount(() => {
timeToRead() timeToRead();
}) });
function timeToRead() { function timeToRead() {
nbSeconds -= 1; nbSeconds -= 1;
nameButton = nbSeconds.toString(); nameButton = nbSeconds.toString();
if ( nbSeconds > 0 ) { if (nbSeconds > 0) {
setTimeout( () => { setTimeout(() => {
timeToRead(); timeToRead();
}, 1000); }, 1000);
} else { } else {
@ -29,68 +29,76 @@
} }
</script> </script>
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}"> <div class="main-ban-message nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2> <h2 class="title-ban-message">
<img src="resources/logos/report.svg" alt="***" /> Important message
<img src="resources/logos/report.svg" alt="***" />
</h2>
<div class="content-ban-message"> <div class="content-ban-message">
<p>{text}</p> <p>{text}</p>
</div> </div>
<div class="footer-ban-message"> <div class="footer-ban-message">
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button> <button
type="button"
class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}"
disabled={!(nameButton === NAME_BUTTON)}
on:click|preventDefault={closeBanMessage}>{nameButton}</button
>
</div> </div>
<!-- svelte-ignore a11y-media-has-caption -->
<audio id="report-message" autoplay> <audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3"> <source src="/resources/objects/report-message.mp3" type="audio/mp3" />
</audio> </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: relative;
top: 15vh; top: 15vh;
height: 70vh; height: 70vh;
width: 60vw; width: 60vw;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-bottom: 0; padding-bottom: 0;
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
h2.title-ban-message { h2.title-ban-message {
flex: 1 1 auto; flex: 1 1 auto;
max-height: 50px; max-height: 50px;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center; text-align: center;
img { img {
height: 50px; height: 50px;
} }
}
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
} }
</style>
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
}
</style>

View file

@ -1,59 +1,61 @@
<script lang="ts"> <script lang="ts">
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore"; import { textMessageContentStore, textMessageVisibleStore } from "../../Stores/TypeMessageStore/TextMessageStore";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html"; import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
const content = JSON.parse($textMessageContentStore); const content = JSON.parse($textMessageContentStore);
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true}); const converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
const NAME_BUTTON = 'Ok'; const NAME_BUTTON = "Ok";
function closeTextMessage() { function closeTextMessage() {
textMessageVisibleStore.set(false); textMessageVisibleStore.set(false);
} }
function onKeyDown(e:KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') { if (e.key === "Escape") {
closeTextMessage(); closeTextMessage();
} }
} }
</script> </script>
<svelte:window on:keydown={onKeyDown}/> <svelte:window on:keydown={onKeyDown} />
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}"> <div class="main-text-message nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<div class="content-text-message"> <div class="content-text-message">
{@html converter.convert()} {@html converter.convert()}
</div> </div>
<div class="footer-text-message"> <div class="footer-text-message">
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button> <button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}
>{NAME_BUTTON}</button
>
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
div.main-text-message { div.main-text-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 25vh; max-height: 25vh;
width: 80vw; width: 80vw;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
padding-bottom: 0; padding-bottom: 0;
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;
div.content-text-message { div.content-text-message {
flex: 1 1 auto; flex: 1 1 auto;
max-height: calc(100% - 50px); max-height: calc(100% - 50px);
color: whitesmoke; color: whitesmoke;
overflow: auto; overflow: auto;
} }
div.footer-text-message { div.footer-text-message {
height: 50px; height: 50px;
text-align: center; text-align: center;
} }
} }
</style> </style>

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from "svelte/transition";
import megaphoneImg from "./images/megaphone.svg"; import megaphoneImg from "./images/megaphone.svg";
import {soundPlayingStore} from "../../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../../Stores/SoundPlayingStore";
import {afterUpdate} from "svelte"; import { afterUpdate } from "svelte";
export let url: string; export let url: string;
let audio: HTMLAudioElement; let audio: HTMLAudioElement;
@ -16,37 +16,37 @@
}); });
</script> </script>
<div class="audio-playing" transition:fly="{{ x: 210, duration: 500 }}"> <div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}>
<img src={megaphoneImg} alt="Audio playing" /> <img src={megaphoneImg} alt="Audio playing" />
<p>Audio message</p> <p>Audio message</p>
<audio bind:this={audio} src={url} on:ended={soundEnded} > <audio bind:this={audio} src={url} on:ended={soundEnded}>
<track kind="captions"> <track kind="captions" />
</audio> </audio>
</div> </div>
<style lang="scss"> <style lang="scss">
/*audio html when audio message playing*/ /*audio html when audio message playing*/
.audio-playing { .audio-playing {
position: absolute; position: absolute;
width: 200px; width: 200px;
height: 54px; height: 54px;
right: 0; right: 0;
top: 40px; top: 40px;
transition: all 0.1s ease-out; transition: all 0.1s ease-out;
background-color: black; background-color: black;
border-radius: 30px 0 0 30px; border-radius: 30px 0 0 30px;
display: inline-flex; display: inline-flex;
img { img {
border-radius: 50%; border-radius: 50%;
background-color: #ffda01; background-color: #ffda01;
padding: 10px; padding: 10px;
} }
p { p {
color: white; color: white;
margin-left: 10px; margin-left: 10px;
margin-top: 14px; margin-top: 14px;
}
} }
}
</style> </style>

View file

@ -1,48 +1,49 @@
<script lang="ts"> <script lang="ts">
import {errorStore} from "../../Stores/ErrorStore"; import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
function close(): boolean {
errorStore.clearMessages();
return false;
}
function close(): boolean {
errorStore.clearClosableMessages();
return false;
}
</script> </script>
<div class="error-div nes-container is-dark is-rounded" open> <div class="error-div nes-container is-dark is-rounded" open>
<p class="nes-text is-error title">Error</p> <p class="nes-text is-error title">Error</p>
<div class="body"> <div class="body">
{#each $errorStore as error} {#each $errorStore as error}
<p>{error}</p> <p>{error.message}</p>
{/each} {/each}
</div>
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
</div> </div>
{#if $hasClosableMessagesInErrorStore}
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
</div>
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">
div.error-div { div.error-div {
pointer-events: auto; pointer-events: auto;
margin-top: 10vh; margin-top: 10vh;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
width: max-content; width: max-content;
max-width: 80vw; max-width: 80vw;
.button-bar { .button-bar {
text-align: center; text-align: center;
} }
.body { .body {
max-height: 50vh; max-height: 50vh;
} }
p { p {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
&.title { &.title {
text-align: center; text-align: center;
}
} }
}
} }
</style> </style>

View file

@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore"; import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import {afterUpdate, onDestroy} from "svelte"; import { afterUpdate, onDestroy } from "svelte";
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte"; import MediaBox from "./MediaBox.svelte";
let cssClass = 'one-col'; let cssClass = "one-col";
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => { const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
const nbUsers = displayableMedias.size; const nbUsers = displayableMedias.size;
if (nbUsers <= 1) { if (nbUsers <= 1) {
cssClass = 'one-col'; cssClass = "one-col";
} else if (nbUsers <= 4) { } else if (nbUsers <= 4) {
cssClass = 'two-col'; cssClass = "two-col";
} else if (nbUsers <= 9) { } else if (nbUsers <= 9) {
cssClass = 'three-col'; cssClass = "three-col";
} else { } else {
cssClass = 'four-col'; cssClass = "four-col";
} }
}); });
@ -25,11 +25,11 @@
afterUpdate(() => { afterUpdate(() => {
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
}) });
</script> </script>
<div class="chat-mode {cssClass}"> <div class="chat-mode {cssClass}">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
<MediaBox streamable={peer}></MediaBox> <MediaBox streamable={peer} />
{/each} {/each}
</div> </div>

View file

@ -1,16 +1,15 @@
<script lang="typescript"> <script lang="typescript">
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore"; import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore";
import {videoFocusStore} from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
import {srcObject} from "./utils"; import { srcObject } from "./utils";
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;
</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> <video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
{/if} {/if}
</div> </div>

View file

@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import {VideoPeer} from "../../WebRtc/VideoPeer"; import { VideoPeer } from "../../WebRtc/VideoPeer";
import VideoMediaBox from "./VideoMediaBox.svelte"; import VideoMediaBox from "./VideoMediaBox.svelte";
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte"; import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer"; import { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte"; import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
import type {Streamable} from "../../Stores/StreamableCollectionStore"; import type { Streamable } from "../../Stores/StreamableCollectionStore";
export let streamable: Streamable; export let streamable: Streamable;
</script> </script>
<div class="media-container"> <div class="media-container">
{#if streamable instanceof VideoPeer} {#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable}/> <VideoMediaBox peer={streamable} />
{:else if streamable instanceof ScreenSharingPeer} {:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable}/> <ScreenSharingMediaBox peer={streamable} />
{:else} {:else}
<LocalStreamMediaBox peer={streamable} cssClass=""/> <LocalStreamMediaBox peer={streamable} cssClass="" />
{/if} {/if}
</div> </div>

View file

@ -1,26 +1,26 @@
<script lang="ts"> <script lang="ts">
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore"; import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import {videoFocusStore} from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
import {afterUpdate} from "svelte"; import { afterUpdate } from "svelte";
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte"; import MediaBox from "./MediaBox.svelte";
afterUpdate(() => { afterUpdate(() => {
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
}) });
</script> </script>
<div class="main-section"> <div class="main-section">
{#if $videoFocusStore } {#if $videoFocusStore}
{#key $videoFocusStore.uniqueId} {#key $videoFocusStore.uniqueId}
<MediaBox streamable={$videoFocusStore}></MediaBox> <MediaBox streamable={$videoFocusStore} />
{/key} {/key}
{/if} {/if}
</div> </div>
<aside class="sidebar"> <aside class="sidebar">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if peer !== $videoFocusStore } {#if peer !== $videoFocusStore}
<MediaBox streamable={peer}></MediaBox> <MediaBox streamable={peer} />
{/if} {/if}
{/each} {/each}
</aside> </aside>

View file

@ -1,33 +1,33 @@
<script lang="ts"> <script lang="ts">
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer"; import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import {videoFocusStore} from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
import {getColorByString, srcObject} from "./utils"; import { getColorByString, srcObject } from "./utils";
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;
</script> </script>
<div class="video-container"> <div class="video-container">
{#if $statusStore === 'connecting'} {#if $statusStore === "connecting"}
<div class="connecting-spinner"></div> <div class="connecting-spinner" />
{/if} {/if}
{#if $statusStore === 'error'} {#if $statusStore === "error"}
<div class="rtc-error"></div> <div class="rtc-error" />
{/if} {/if}
{#if $streamStore === null} {#if $streamStore === null}
<i style="background-color: {getColorByString(name)};">{name}</i> <i style="background-color: {getColorByString(name)};">{name}</i>
{:else} {:else}
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
{/if} {/if}
</div> </div>
<style lang="scss"> <style lang="scss">
.video-container { .video-container {
video { video {
width: 100%; width: 100%;
}
} }
}
</style> </style>

View file

@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import type {VideoPeer} from "../../WebRtc/VideoPeer"; import type { VideoPeer } from "../../WebRtc/VideoPeer";
import SoundMeterWidget from "../SoundMeterWidget.svelte"; import SoundMeterWidget from "../SoundMeterWidget.svelte";
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 { 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 Woka from "../Woka/Woka.svelte";
export let peer: VideoPeer; export let peer: VideoPeer;
let streamStore = peer.streamStore; let streamStore = peer.streamStore;
@ -15,32 +17,55 @@
let constraintStore = peer.constraintsStore; let constraintStore = peer.constraintsStore;
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 });
} }
</script> </script>
<div class="video-container"> <div class="video-container">
{#if $statusStore === 'connecting'} {#if $statusStore === "connecting"}
<div class="connecting-spinner"></div> <div class="connecting-spinner" />
{/if} {/if}
{#if $statusStore === 'error'} {#if $statusStore === "error"}
<div class="rtc-error"></div> <div class="rtc-error" />
{/if}
{#if !$constraintStore || $constraintStore.video === false}
<i style="background-color: {getColorByString(name)};">{name}</i>
{/if} {/if}
<!-- {#if !$constraintStore || $constraintStore.video === false} -->
<i
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}"
style="background-color: {getColorByString(name)};"
>
<span>{peer.userName}</span>
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
</i>
<!-- {/if} -->
{#if $constraintStore && $constraintStore.audio === false} {#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} class="active" alt="Muted"> <img src={microphoneCloseImg} class="active" alt="Muted" />
{/if} {/if}
<button class="report" on:click={() => openReport(peer)}> <button class="report" on:click={() => openReport(peer)}>
<img alt="Report this user" src={reportImg}> <img alt="Report this user" src={reportImg} />
<span>Report/Block</span> <span>Report/Block</span>
</button> </button>
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
<img src={blockSignImg} class="block-logo" alt="Block" /> <img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false} {#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget> <SoundMeterWidget stream={$streamStore} />
{/if} {/if}
</div> </div>
<style>
.container {
display: flex;
flex-direction: column;
padding-top: 15px;
}
.minimized {
left: auto;
transform: scale(0.5);
opacity: 0.5;
}
.woka-icon {
margin-right: 3px;
}
</style>

View file

@ -1,23 +1,22 @@
<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">
.video-overlay { .video-overlay {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
</style> </style>

View file

@ -1,11 +1,11 @@
<script lang="typescript"> <script lang="typescript">
import { fly } from 'svelte/transition'; import { fly } from "svelte/transition";
import {requestVisitCardsStore} from "../../Stores/GameStore"; import { requestVisitCardsStore } from "../../Stores/GameStore";
import {onMount} from "svelte"; import { onMount } from "svelte";
export let visitCardUrl: string; export let visitCardUrl: string;
let w = '500px'; let w = "500px";
let h = '250px'; let h = "250px";
let hidden = true; let hidden = true;
let cvIframe: HTMLIFrameElement; let cvIframe: HTMLIFrameElement;
@ -13,73 +13,81 @@
requestVisitCardsStore.set(null); requestVisitCardsStore.set(null);
} }
function handleIframeMessage(message:any) { function handleIframeMessage(message: MessageEvent) {
if (message.data.type === 'cvIframeSize') { if (message.data.type === "cvIframeSize") {
w = (message.data.data.w) + 'px'; w = message.data.data.w + "px";
h = (message.data.data.h) + 'px'; h = message.data.data.h + "px";
} }
} }
onMount(() => { onMount(() => {
cvIframe.onload = () => hidden = false cvIframe.onload = () => (hidden = false);
cvIframe.onerror = () => hidden = false cvIframe.onerror = () => (hidden = false);
}) });
</script> </script>
<style lang="scss"> <section class="visitCard" transition:fly={{ y: -200, duration: 1000 }} style="width: {w}">
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
margin:auto;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.visitCard {
pointer-events: all;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
margin-top: 200px;
max-width: 80vw;
iframe {
border: 0;
max-width: 80vw;
overflow: hidden;
&.hidden {
visibility: hidden;
position: absolute;
}
}
button {
float: right;
}
}
</style>
<section class="visitCard" transition:fly="{{ y: -200, duration: 1000 }}" style="width: {w}">
{#if hidden} {#if hidden}
<div class="loader"></div> <div class="loader" />
{/if} {/if}
<iframe title="visitCard" src={visitCardUrl} allow="clipboard-read; clipboard-write self {visitCardUrl}" style="width: {w}; height: {h}" class:hidden={hidden} bind:this={cvIframe}></iframe> <iframe
title="visitCard"
src={visitCardUrl}
allow="clipboard-read; clipboard-write self {visitCardUrl}"
style="width: {w}; height: {h}"
class:hidden
bind:this={cvIframe}
/>
{#if !hidden} {#if !hidden}
<div class="buttonContainer"> <div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button> <button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
</div> </div>
{/if} {/if}
</section> </section>
<svelte:window on:message={handleIframeMessage}/> <svelte:window on:message={handleIframeMessage} />
<style lang="scss">
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
margin: auto;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.visitCard {
pointer-events: all;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
margin-top: 200px;
max-width: 80vw;
iframe {
border: 0;
max-width: 80vw;
overflow: hidden;
&.hidden {
visibility: hidden;
position: absolute;
}
}
button {
float: right;
}
}
</style>

View file

@ -1,37 +1,39 @@
<script lang="typescript"> <script lang="typescript">
import { fly } from 'svelte/transition'; import { fly } from "svelte/transition";
import {userIsAdminStore} from "../../Stores/GameStore"; import { userIsAdminStore } from "../../Stores/GameStore";
import {ADMIN_URL} from "../../Enum/EnvironmentVariable"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL+'/pricing'; const upgradeLink = ADMIN_URL + "/pricing";
</script> </script>
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}"> <main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
<h2>Warning!</h2> <h2>Warning!</h2>
{#if $userIsAdminStore} {#if $userIsAdminStore}
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p> <p>
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
>here</a
>
</p>
{:else} {:else}
<p>This world is close to its limit!</p> <p>This world is close to its limit!</p>
{/if} {/if}
</main> </main>
<style lang="scss"> <style lang="scss">
main.warningMain { main.warningMain {
pointer-events: auto; pointer-events: auto;
width: 100vw; width: 100vw;
background-color: red; background-color: red;
text-align: center; text-align: center;
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
font-family: Lato; font-family: Lato;
min-width: 300px; min-width: 300px;
opacity: 0.9; opacity: 0.9;
z-index: 2; z-index: 2;
h2 { h2 {
padding: 5px; padding: 5px;
} }
} }
</style> </style>

View file

@ -0,0 +1,45 @@
<script lang="typescript">
import { onDestroy } from "svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
export let userId: number;
export let placeholderSrc: string;
export let width: string = "62px";
export let height: string = "62px";
const gameScene = gameManager.getCurrentGameScene();
let playerWokaPictureStore;
if (userId === -1) {
playerWokaPictureStore = gameScene.CurrentPlayer.pictureStore;
} else {
playerWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(userId, (item) => item.pictureStore);
}
let src = placeholderSrc;
const unsubscribe = playerWokaPictureStore.subscribe((source) => {
src = source ?? placeholderSrc;
});
onDestroy(unsubscribe);
</script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
<style>
img {
display: inline-block;
pointer-events: auto;
width: var(--theme-width);
height: var(--theme-height);
margin: 0;
padding: 0;
position: static;
left: 0;
bottom: 0;
right: 0;
top: 0;
image-rendering: pixelated;
}
</style>

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
export let game: Game; export let game: Game;
@ -21,72 +21,81 @@
function customizeScene() { function customizeScene() {
selectCharacterScene.nextSceneToCustomizeScene(); selectCharacterScene.nextSceneToCustomizeScene();
} }
</script> </script>
<form class="selectCharacterScene"> <form class="selectCharacterScene">
<section class="text-center"> <section class="text-center">
<h2>Select your WOKA</h2> <h2>Select your WOKA</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> &lt; </button> <button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button> &lt;
</button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}>
&gt;
</button>
</section> </section>
<section class="action"> <section class="action">
<button type="submit" class="selectCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ cameraScene }>Continue</button> <button
<button type="submit" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" on:click|preventDefault={ customizeScene }>Customize your WOKA</button> type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>Continue</button
>
<button
type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>Customize your WOKA</button
>
</section> </section>
</form> </form>
<style lang="scss"> <style lang="scss">
form.selectCharacterScene { form.selectCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
margin: 1px; pointer-events: auto;
} color: #ebeeee;
&.text-center { section {
text-align: center; margin: 10px;
}
button.selectCharacterButton { &.action {
position: absolute; text-align: center;
top: 33vh; margin-top: 55vh;
margin: 0; }
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
}
button {
font-family: "Press Start 2P";
&.selectCharacterButtonLeft {
left: 33vw;
}
&.selectCharacterButtonRight {
right: 33vw;
}
}
} }
button { @media only screen and (max-width: 800px) {
font-family: "Press Start 2P"; form.selectCharacterScene button.selectCharacterButtonLeft {
left: 5vw;
&.selectCharacterButtonLeft { }
left: 33vw; form.selectCharacterScene button.selectCharacterButtonRight {
} right: 5vw;
}
&.selectCharacterButtonRight {
right: 33vw;
}
} }
}
@media only screen and (max-width: 800px) {
form.selectCharacterScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCharacterScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style> </style>

View file

@ -0,0 +1,43 @@
import axios from "axios";
import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore";
/**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
*/
export const axiosWithRetry = axios.create();
axiosWithRetry.defaults.raxConfig = {
instance: axiosWithRetry,
retry: Infinity,
noResponseRetries: Infinity,
maxRetryAfter: 60_000,
statusCodesToRetry: [
[100, 199],
[429, 429],
[501, 599],
],
// You can detect when a retry is happening, and figure out how many
// retry attempts have been made
onRetryAttempt: (err) => {
const cfg = rax.getConfig(err);
console.log(err);
console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage("Unable to connect to WorkAdventure. Are you connected to internet?", {
closable: false,
id: "axios_retry",
});
},
};
axiosWithRetry.interceptors.response.use((res) => {
if (res.status < 400) {
errorStore.clearMessageById("axios_retry");
}
return res;
});
const interceptorId = rax.attach(axiosWithRetry);

View file

@ -10,6 +10,7 @@ import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore"; import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore"; import { userIsConnected } from "../Stores/MenuStore";
import { analyticsClient } from "../Administration/AnalyticsClient"; import { analyticsClient } from "../Administration/AnalyticsClient";
import { axiosWithRetry } from "./AxiosUtils";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@ -132,13 +133,14 @@ class ConnectionManager {
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
const query = urlParams.toString();
this._currentRoom = await Room.createRoom( this._currentRoom = await Room.createRoom(
new URL( new URL(
window.location.protocol + window.location.protocol +
"//" + "//" +
window.location.host + window.location.host +
roomUrl + roomUrl +
urlParams.toString() + //use urlParams because the token param must be deleted (query ? "?" + query : "") + //use urlParams because the token param must be deleted
window.location.hash window.location.hash
) )
); );
@ -163,12 +165,13 @@ class ConnectionManager {
console.error(err); console.error(err);
} }
} else { } else {
const query = urlParams.toString();
roomPath = roomPath =
window.location.protocol + window.location.protocol +
"//" + "//" +
window.location.host + window.location.host +
window.location.pathname + window.location.pathname +
urlParams.toString() + //use urlParams because the token param must be deleted (query ? "?" + query : "") + //use urlParams because the token param must be deleted
window.location.hash; window.location.hash;
} }
@ -230,7 +233,7 @@ class ConnectionManager {
} }
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data); const data = await axiosWithRetry.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, [], data.email); this.localUser = new LocalUser(data.userUuid, [], data.email);
this.authToken = data.authToken; this.authToken = data.authToken;
if (!isBenchmark) { if (!isBenchmark) {

View file

@ -13,6 +13,7 @@ const audioPlayerVolumeKey = "audioVolume";
const audioPlayerMuteKey = "audioMute"; const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = "helpCameraSettingsShown"; const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen"; const fullscreenKey = "fullscreen";
const forceCowebsiteTriggerKey = "forceCowebsiteTrigger";
const lastRoomUrl = "lastRoomUrl"; const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken"; const authToken = "authToken";
const state = "state"; const state = "state";
@ -120,6 +121,13 @@ class LocalUserStore {
return localStorage.getItem(fullscreenKey) === "true"; return localStorage.getItem(fullscreenKey) === "true";
} }
setForceCowebsiteTrigger(value: boolean): void {
localStorage.setItem(forceCowebsiteTriggerKey, value.toString());
}
getForceCowebsiteTrigger(): boolean {
return localStorage.getItem(forceCowebsiteTriggerKey) === "true";
}
setLastRoomUrl(roomUrl: string): void { setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString()); localStorage.setItem(lastRoomUrl, roomUrl.toString());
if ("caches" in window) { if ("caches" in window) {

View file

@ -1,7 +1,10 @@
import * as rax from "retry-axios";
import Axios from "axios"; import Axios from "axios";
import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import type { CharacterTexture } from "./LocalUser"; import type { CharacterTexture } from "./LocalUser";
import { localUserStore } from "./LocalUserStore"; import { localUserStore } from "./LocalUserStore";
import axios from "axios";
import { axiosWithRetry } from "./AxiosUtils";
export class MapDetail { export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {} constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
@ -90,7 +93,7 @@ export class Room {
private async getMapDetail(): Promise<MapDetail | RoomRedirect> { private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
try { try {
const result = await Axios.get(`${PUSHER_URL}/map`, { const result = await axiosWithRetry.get(`${PUSHER_URL}/map`, {
params: { params: {
playUri: this.roomUrl.toString(), playUri: this.roomUrl.toString(),
authToken: localUserStore.getAuthToken(), authToken: localUserStore.getAuthToken(),
@ -113,11 +116,12 @@ export class Room {
this._contactPage = data.contactPage || CONTACT_URL; this._contactPage = data.contactPage || CONTACT_URL;
return new MapDetail(data.mapUrl, data.textures); return new MapDetail(data.mapUrl, data.textures);
} catch (e) { } catch (e) {
console.error("Error => getMapDetail", e, e.response); if (axios.isAxiosError(e) && e.response?.status == 401 && e.response?.data === "Token decrypted error") {
//TODO fix me and manage Error class console.warn("JWT token sent could not be decrypted. Maybe it expired?");
if (e.response?.data === "Token decrypted error") {
localUserStore.setAuthToken(null); localUserStore.setAuthToken(null);
window.location.assign("/login"); window.location.assign("/login");
} else {
console.error("Error => getMapDetail", e, e.response);
} }
throw e; throw e;
} }

View file

@ -68,6 +68,7 @@ export class RoomConnection implements RoomConnection {
private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
private closed: boolean = false; private closed: boolean = false;
private tags: string[] = []; private tags: string[] = [];
private _userRoomToken: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static setWebsocketFactory(websocketFactory: (url: string) => any): void { public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
@ -211,6 +212,7 @@ export class RoomConnection implements RoomConnection {
this.userId = roomJoinedMessage.getCurrentuserid(); this.userId = roomJoinedMessage.getCurrentuserid();
this.tags = roomJoinedMessage.getTagList(); this.tags = roomJoinedMessage.getTagList();
this._userRoomToken = roomJoinedMessage.getUserroomtoken();
this.dispatch(EventMessage.CONNECT, { this.dispatch(EventMessage.CONNECT, {
connection: this, connection: this,
@ -255,6 +257,9 @@ export class RoomConnection implements RoomConnection {
warningContainerStore.activateWarningContainer(); warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
} else { } else {
throw new Error("Unknown message received"); throw new Error("Unknown message received");
} }
@ -710,4 +715,8 @@ export class RoomConnection implements RoomConnection {
public getAllTags(): string[] { public getAllTags(): string[] {
return this.tags; return this.tags;
} }
public get userRoomToken(): string | undefined {
return this._userRoomToken;
}
} }

View file

@ -1,6 +1,9 @@
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import Container = Phaser.GameObjects.Container; import Container = Phaser.GameObjects.Container;
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation"; import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
import { TexturesHelper } from "../Helpers/TexturesHelper";
import { Writable, writable } from "svelte/store";
import type { PictureStore } from "../../Stores/PictureStore";
export interface CompanionStatus { export interface CompanionStatus {
x: number; x: number;
@ -21,6 +24,7 @@ export class Companion extends Container {
private companionName: string; private companionName: string;
private direction: PlayerAnimationDirections; private direction: PlayerAnimationDirections;
private animationType: PlayerAnimationTypes; private animationType: PlayerAnimationTypes;
private readonly _pictureStore: Writable<string | 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: Promise<string>) {
super(scene, x + 14, y + 4); super(scene, x + 14, y + 4);
@ -35,10 +39,14 @@ export class Companion extends Container {
this.animationType = PlayerAnimationTypes.Idle; this.animationType = PlayerAnimationTypes.Idle;
this.companionName = name; this.companionName = name;
this._pictureStore = writable(undefined);
texturePromise.then((resource) => { texturePromise.then((resource) => {
this.addResource(resource); this.addResource(resource);
this.invisible = false; this.invisible = false;
return this.getSnapshot().then((htmlImageElementSrc) => {
this._pictureStore.set(htmlImageElementSrc);
});
}); });
this.scene.physics.world.enableBody(this); this.scene.physics.world.enableBody(this);
@ -123,6 +131,22 @@ export class Companion extends Container {
}; };
} }
public async getSnapshot(): Promise<string> {
const sprites = Array.from(this.sprites.values()).map((sprite) => {
return { sprite, frame: 1 };
});
return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => {
console.warn(reason);
for (const sprite of this.sprites.values()) {
// it can be either cat or dog prefix
if (sprite.texture.key.includes("cat") || sprite.texture.key.includes("dog")) {
return this.scene.textures.getBase64(sprite.texture.key);
}
}
return "cat1";
});
}
private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void { private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void {
if (this.invisible) return; if (this.invisible) return;
@ -220,4 +244,8 @@ export class Companion extends Container {
super.destroy(); super.destroy();
} }
public get pictureStore(): PictureStore {
return this._pictureStore;
}
} }

View file

@ -8,10 +8,12 @@ import { TextureError } from "../../Exception/TextureError";
import { Companion } from "../Companion/Companion"; import { Companion } from "../Companion/Companion";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import { waScaleManager } from "../Services/WaScaleManager";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { isSilentStore } from "../../Stores/MediaStore"; import { isSilentStore } from "../../Stores/MediaStore";
import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
import { TexturesHelper } from "../Helpers/TexturesHelper";
import type { PictureStore } from "../../Stores/PictureStore";
import { Writable, writable } from "svelte/store";
const playerNameY = -25; const playerNameY = -25;
@ -37,6 +39,7 @@ export abstract class Character extends Container {
private emote: Phaser.GameObjects.DOMElement | null = null; private emote: Phaser.GameObjects.DOMElement | null = null;
private emoteTween: Phaser.Tweens.Tween | null = null; private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene; scene: GameScene;
private readonly _pictureStore: Writable<string | undefined>;
constructor( constructor(
scene: GameScene, scene: GameScene,
@ -57,6 +60,7 @@ export abstract class Character extends Container {
this.invisible = true; this.invisible = true;
this.sprites = new Map<string, Sprite>(); this.sprites = new Map<string, Sprite>();
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 texturesPromise
@ -64,6 +68,9 @@ export abstract class Character extends Container {
this.addTextures(textures, frame); this.addTextures(textures, frame);
this.invisible = false; this.invisible = false;
this.playAnimation(direction, moving); this.playAnimation(direction, moving);
return this.getSnapshot().then((htmlImageElementSrc) => {
this._pictureStore.set(htmlImageElementSrc);
});
}) })
.catch(() => { .catch(() => {
return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => { return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => {
@ -117,8 +124,20 @@ export abstract class Character extends Container {
} }
} }
private getOutlinePlugin(): OutlinePipelinePlugin | undefined { private async getSnapshot(): Promise<string> {
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; const sprites = Array.from(this.sprites.values()).map((sprite) => {
return { sprite, frame: 1 };
});
return TexturesHelper.getSnapshot(this.scene, ...sprites).catch((reason) => {
console.warn(reason);
for (const sprite of this.sprites.values()) {
// we can be sure that either predefined woka or body texture is at this point loaded
if (sprite.texture.key.includes("color") || sprite.texture.key.includes("male")) {
return this.scene.textures.getBase64(sprite.texture.key);
}
}
return "male1";
});
} }
public addCompanion(name: string, texturePromise?: Promise<string>): void { public addCompanion(name: string, texturePromise?: Promise<string>): void {
@ -154,6 +173,10 @@ export abstract class Character extends Container {
} }
} }
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
}
private getPlayerAnimations(name: string): AnimationData[] { private getPlayerAnimations(name: string): AnimationData[] {
return [ return [
{ {
@ -374,4 +397,8 @@ export abstract class Character extends Container {
this.emote = null; this.emote = null;
this.playerName.setVisible(true); this.playerName.setVisible(true);
} }
public get pictureStore(): PictureStore {
return this._pictureStore;
}
} }

View file

@ -0,0 +1,178 @@
import { Easing } from "../../types";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type { Box } from "../../WebRtc/LayoutManager";
import type { Player } from "../Player/Player";
import type { WaScaleManager } from "../Services/WaScaleManager";
import type { GameScene } from "./GameScene";
export enum CameraMode {
Free = "Free",
Follow = "Follow",
Focus = "Focus",
}
export class CameraManager extends Phaser.Events.EventEmitter {
private scene: GameScene;
private camera: Phaser.Cameras.Scene2D.Camera;
private cameraBounds: { x: number; y: number };
private waScaleManager: WaScaleManager;
private cameraMode: CameraMode = CameraMode.Free;
private restoreZoomTween?: Phaser.Tweens.Tween;
private startFollowTween?: Phaser.Tweens.Tween;
private cameraFollowTarget?: { x: number; y: number };
constructor(scene: GameScene, cameraBounds: { x: number; y: number }, waScaleManager: WaScaleManager) {
super();
this.scene = scene;
this.camera = scene.cameras.main;
this.cameraBounds = cameraBounds;
this.waScaleManager = waScaleManager;
this.initCamera();
this.bindEventHandlers();
}
public destroy(): void {
this.scene.game.events.off("wa-scale-manager:refresh-focus-on-target");
super.destroy();
}
public getCamera(): Phaser.Cameras.Scene2D.Camera {
return this.camera;
}
public enterFocusMode(
focusOn: { x: number; y: number; width: number; height: number },
margin: number = 0,
duration: number = 1000
): void {
this.setCameraMode(CameraMode.Focus);
this.waScaleManager.saveZoom();
this.waScaleManager.setFocusTarget(focusOn);
this.restoreZoomTween?.stop();
this.startFollowTween?.stop();
const marginMult = 1 + margin;
const targetZoomModifier = this.waScaleManager.getTargetZoomModifierFor(
focusOn.width * marginMult,
focusOn.height * marginMult
);
const currentZoomModifier = this.waScaleManager.zoomModifier;
const zoomModifierChange = targetZoomModifier - currentZoomModifier;
this.camera.stopFollow();
this.cameraFollowTarget = undefined;
this.camera.pan(
focusOn.x + focusOn.width * 0.5 * marginMult,
focusOn.y + focusOn.height * 0.5 * marginMult,
duration,
Easing.SineEaseOut,
true,
(camera, progress, x, y) => {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
}
);
}
public leaveFocusMode(player: Player): void {
this.waScaleManager.setFocusTarget();
this.startFollow(player, 1000);
this.restoreZoom(1000);
}
public startFollow(target: object | Phaser.GameObjects.GameObject, duration: number = 0): void {
this.cameraFollowTarget = target as { x: number; y: number };
this.setCameraMode(CameraMode.Follow);
if (duration === 0) {
this.camera.startFollow(target, true);
return;
}
const oldPos = { x: this.camera.scrollX, y: this.camera.scrollY };
this.startFollowTween = this.scene.tweens.addCounter({
from: 0,
to: 1,
duration,
ease: Easing.SineEaseOut,
onUpdate: (tween: Phaser.Tweens.Tween) => {
if (!this.cameraFollowTarget) {
return;
}
const shiftX =
(this.cameraFollowTarget.x - this.camera.worldView.width * 0.5 - oldPos.x) * tween.getValue();
const shiftY =
(this.cameraFollowTarget.y - this.camera.worldView.height * 0.5 - oldPos.y) * tween.getValue();
this.camera.setScroll(oldPos.x + shiftX, oldPos.y + shiftY);
},
onComplete: () => {
this.camera.startFollow(target, true);
},
});
}
/**
* Updates the offset of the character compared to the center of the screen according to the layout manager
* (tries to put the character in the center of the remaining space if there is a discussion going on.
*/
public updateCameraOffset(array: Box): void {
const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
// Let's put this in Game coordinates by applying the zoom level:
this.camera.setFollowOffset(
((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scene.scale.zoom,
((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scene.scale.zoom
);
}
public isCameraLocked(): boolean {
return this.cameraMode === CameraMode.Focus;
}
private setCameraMode(mode: CameraMode): void {
if (this.cameraMode === mode) {
return;
}
this.cameraMode = mode;
}
private restoreZoom(duration: number = 0): void {
if (duration === 0) {
this.waScaleManager.zoomModifier = this.waScaleManager.getSaveZoom();
return;
}
this.restoreZoomTween?.stop();
this.restoreZoomTween = this.scene.tweens.addCounter({
from: this.waScaleManager.zoomModifier,
to: this.waScaleManager.getSaveZoom(),
duration,
ease: Easing.SineEaseOut,
onUpdate: (tween: Phaser.Tweens.Tween) => {
this.waScaleManager.zoomModifier = tween.getValue();
},
});
}
private initCamera() {
this.camera = this.scene.cameras.main;
this.camera.setBounds(0, 0, this.cameraBounds.x, this.cameraBounds.y);
}
private bindEventHandlers(): void {
this.scene.game.events.on(
"wa-scale-manager:refresh-focus-on-target",
(focusOn: { x: number; y: number; width: number; height: number }) => {
if (!focusOn) {
return;
}
this.camera.centerOn(focusOn.x + focusOn.width * 0.5, focusOn.y + focusOn.height * 0.5);
}
);
}
}

View file

@ -1,8 +1,15 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap"; import type {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapObjectLayer,
ITiledMapProperty,
} from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import { MathUtils } from "../../Utils/MathUtils";
export type PropertyChangeCallback = ( export type PropertyChangeCallback = (
newValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined,
@ -15,24 +22,48 @@ export type layerChangeCallback = (
allLayersOnNewPosition: Array<ITiledMapLayer> allLayersOnNewPosition: Array<ITiledMapLayer>
) => void; ) => void;
export type zoneChangeCallback = (
zonesChangedByAction: Array<ITiledMapObject>,
allZonesOnNewPosition: Array<ITiledMapObject>
) => void;
/** /**
* A wrapper around a ITiledMap interface to provide additional capabilities. * A wrapper around a ITiledMap interface to provide additional capabilities.
* It is used to handle layer properties. * It is used to handle layer properties.
*/ */
export class GameMap { export class GameMap {
// oldKey is the index of the previous tile. /**
* oldKey is the index of the previous tile.
*/
private oldKey: number | undefined; private oldKey: number | undefined;
// key is the index of the current tile. /**
* key is the index of the current tile.
*/
private key: number | undefined; private key: number | undefined;
/**
* oldPosition is the previous position of the player.
*/
private oldPosition: { x: number; y: number } | undefined;
/**
* position is the current position of the player.
*/
private position: { x: number; y: number } | undefined;
private lastProperties = new Map<string, string | boolean | number>(); private lastProperties = new Map<string, string | boolean | number>();
private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>(); private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
private enterLayerCallbacks = Array<layerChangeCallback>(); private enterLayerCallbacks = Array<layerChangeCallback>();
private leaveLayerCallbacks = Array<layerChangeCallback>(); private leaveLayerCallbacks = Array<layerChangeCallback>();
private enterZoneCallbacks = Array<zoneChangeCallback>();
private leaveZoneCallbacks = Array<zoneChangeCallback>();
private tileNameMap = new Map<string, number>(); private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {}; private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
public readonly flatLayers: ITiledMapLayer[]; public readonly flatLayers: ITiledMapLayer[];
public readonly tiledObjects: ITiledMapObject[];
public readonly phaserLayers: TilemapLayer[] = []; public readonly phaserLayers: TilemapLayer[] = [];
public readonly zones: ITiledMapObject[] = [];
public exitUrls: Array<string> = []; public exitUrls: Array<string> = [];
@ -44,6 +75,9 @@ export class GameMap {
terrains: Array<Phaser.Tilemaps.Tileset> terrains: Array<Phaser.Tilemaps.Tileset>
) { ) {
this.flatLayers = flattenGroupLayersMap(map); this.flatLayers = flattenGroupLayersMap(map);
this.tiledObjects = this.getObjectsFromLayers(this.flatLayers);
this.zones = this.tiledObjects.filter((object) => object.type === "zone");
let depth = -2; let depth = -2;
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if (layer.type === "tilelayer") { if (layer.type === "tilelayer") {
@ -88,6 +122,10 @@ export class GameMap {
* This will trigger events if properties are changing. * This will trigger events if properties are changing.
*/ */
public setPosition(x: number, y: number) { public setPosition(x: number, y: number) {
this.oldPosition = this.position;
this.position = { x, y };
this.triggerZonesChange();
this.oldKey = this.key; this.oldKey = this.key;
const xMap = Math.floor(x / this.map.tilewidth); const xMap = Math.floor(x / this.map.tilewidth);
@ -126,7 +164,7 @@ export class GameMap {
} }
} }
private triggerLayersChange() { private triggerLayersChange(): void {
const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : []; const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : [];
const layersByNewKey = this.key ? this.getLayersByKey(this.key) : []; const layersByNewKey = this.key ? this.getLayersByKey(this.key) : [];
@ -155,6 +193,53 @@ export class GameMap {
} }
} }
/**
* We use Tiled Objects with type "zone" as zones with defined x, y, width and height for easier event triggering.
*/
private triggerZonesChange(): void {
const zonesByOldPosition = this.oldPosition
? this.zones.filter((zone) => {
if (!this.oldPosition) {
return false;
}
return MathUtils.isOverlappingWithRectangle(this.oldPosition, zone);
})
: [];
const zonesByNewPosition = this.position
? this.zones.filter((zone) => {
if (!this.position) {
return false;
}
return MathUtils.isOverlappingWithRectangle(this.position, zone);
})
: [];
const enterZones = new Set(zonesByNewPosition);
const leaveZones = new Set(zonesByOldPosition);
enterZones.forEach((zone) => {
if (leaveZones.has(zone)) {
leaveZones.delete(zone);
enterZones.delete(zone);
}
});
if (enterZones.size > 0) {
const zonesArray = Array.from(enterZones);
for (const callback of this.enterZoneCallbacks) {
callback(zonesArray, zonesByNewPosition);
}
}
if (leaveZones.size > 0) {
const zonesArray = Array.from(leaveZones);
for (const callback of this.leaveZoneCallbacks) {
callback(zonesArray, zonesByNewPosition);
}
}
}
public getCurrentProperties(): Map<string, string | boolean | number> { public getCurrentProperties(): Map<string, string | boolean | number> {
return this.lastProperties; return this.lastProperties;
} }
@ -251,6 +336,20 @@ export class GameMap {
this.leaveLayerCallbacks.push(callback); this.leaveLayerCallbacks.push(callback);
} }
/**
* Registers a callback called when the user moves inside another zone.
*/
public onEnterZone(callback: zoneChangeCallback) {
this.enterZoneCallbacks.push(callback);
}
/**
* Registers a callback called when the user moves outside another zone.
*/
public onLeaveZone(callback: zoneChangeCallback) {
this.leaveZoneCallbacks.push(callback);
}
public findLayer(layerName: string): ITiledMapLayer | undefined { public findLayer(layerName: string): ITiledMapLayer | undefined {
return this.flatLayers.find((layer) => layer.name === layerName); return this.flatLayers.find((layer) => layer.name === layerName);
} }
@ -362,4 +461,17 @@ export class GameMap {
this.trigger(oldPropName, oldPropValue, undefined, emptyProps); this.trigger(oldPropName, oldPropValue, undefined, emptyProps);
} }
} }
private getObjectsFromLayers(layers: ITiledMapLayer[]): ITiledMapObject[] {
const objects: ITiledMapObject[] = [];
const objectLayers = layers.filter((layer) => layer.type === "objectgroup");
for (const objectLayer of objectLayers) {
if (objectLayer.type === "objectgroup") {
objects.push(...objectLayer.objects);
}
}
return objects;
}
} }

View file

@ -4,6 +4,7 @@ import { scriptUtils } from "../../Api/ScriptUtils";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager"; import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap"; import type { ITiledMapLayer } from "../Map/ITiledMap";
@ -33,7 +34,8 @@ export class GameMapPropertiesListener {
} }
if (typeof newValue == "string" && newValue.length) { if (typeof newValue == "string" && newValue.length) {
const openWebsiteTriggerValue = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER); const openWebsiteTriggerValue = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER);
if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { const forceTrigger = localUserStore.getForceCowebsiteTrigger();
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 = "Press SPACE or touch here to open web site in new tab"; message = "Press SPACE or touch here to open web site in new tab";
@ -135,7 +137,8 @@ export class GameMapPropertiesListener {
layoutManagerActionStore.removeAction(actionUuid); layoutManagerActionStore.removeAction(actionUuid);
}; };
if (websiteTriggerProperty && websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) { const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) {
if (!websiteTriggerMessageProperty) { if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site"; websiteTriggerMessageProperty = "Press SPACE or touch here to open web site";
} }

View file

@ -1,7 +1,54 @@
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import AnimatedTiles from "phaser-animated-tiles";
import { Queue } from "queue-typescript";
import { get } from "svelte/store";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
import { iframeListener } from "../../Api/IframeListener";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { urlManager } from "../../Url/UrlManager";
import { mediaManager } from "../../WebRtc/MediaManager";
import { UserInputManager } from "../UserInput/UserInputManager";
import { gameManager } from "./GameManager";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import { soundManager } from "./SoundManager";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { iframeListener } from "../../Api/IframeListener";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { TextureError } from "../../Exception/TextureError";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { Loader } from "../Components/Loader";
import { RemotePlayer } from "../Entity/RemotePlayer";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { StartPositionCalculator } from "./StartPositionCalculator";
import { PropertyUtils } from "../Map/PropertyUtils";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { GameMapProperties } from "./GameMapProperties";
import type { import type {
GroupCreatedUpdatedMessageInterface, GroupCreatedUpdatedMessageInterface,
MessageUserJoined, MessageUserJoined,
@ -12,86 +59,37 @@ import type {
PositionInterface, PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { Queue } from "queue-typescript";
import { Box, ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import type { RoomConnection } from "../../Connexion/RoomConnection"; import type { RoomConnection } from "../../Connexion/RoomConnection";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { urlManager } from "../../Url/UrlManager";
import { TextureError } from "../../Exception/TextureError";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { Loader } from "../Components/Loader";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem"; import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { UserInputManager } from "../UserInput/UserInputManager";
import type { AddPlayerInterface } from "./AddPlayerInterface"; import type { AddPlayerInterface } from "./AddPlayerInterface";
import { gameManager } from "./GameManager"; import { CameraManager } from "./CameraManager";
import { GameMap } from "./GameMap"; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import { PlayerMovement } from "./PlayerMovement"; import type { Character } from "../Entity/Character";
import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
import { peerStore } from "../../Stores/PeerStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { playersStore } from "../../Stores/PlayersStore";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture; import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement; import DOMElement = Phaser.GameObjects.DOMElement;
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles";
import { StartPositionCalculator } from "./StartPositionCalculator";
import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import {
audioManagerFileStore,
audioManagerVisibilityStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import { PropertyUtils } from "../Map/PropertyUtils";
import Tileset = Phaser.Tilemaps.Tileset; import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { GameMapPropertiesListener } from "./GameMapPropertiesListener";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { get } from "svelte/store";
import { contactPageStore } from "../../Stores/MenuStore";
import { GameMapProperties } from "./GameMapProperties";
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile; import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
import Camera = Phaser.Cameras.Scene2D.Camera; import Camera = Phaser.Cameras.Scene2D.Camera;
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
reconnecting: boolean; reconnecting: boolean;
@ -131,7 +129,7 @@ export class GameScene extends DirtyScene {
Terrains: Array<Phaser.Tilemaps.Tileset>; Terrains: Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player; CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey: Map<number, RemotePlayer> = new Map<number, RemotePlayer>(); MapPlayersByKey: MapStore<number, RemotePlayer> = new MapStore<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap; Map!: Phaser.Tilemaps.Tilemap;
Objects!: Array<Phaser.Physics.Arcade.Sprite>; Objects!: Array<Phaser.Physics.Arcade.Sprite>;
mapFile!: ITiledMap; mapFile!: ITiledMap;
@ -200,6 +198,7 @@ export class GameScene extends DirtyScene {
private pinchManager: PinchManager | undefined; private pinchManager: PinchManager | undefined;
private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time. private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time.
private emoteManager!: EmoteManager; private emoteManager!: EmoteManager;
private cameraManager!: CameraManager;
private preloading: boolean = true; private preloading: boolean = true;
private startPositionCalculator!: StartPositionCalculator; private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager; private sharedVariablesManager!: SharedVariablesManager;
@ -273,7 +272,9 @@ export class GameScene extends DirtyScene {
// 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP. // 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP.
// So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes) // So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes)
// See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure // See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure
const url = new URL(file.src); const base = new URL(window.location.href);
base.pathname = "";
const url = new URL(file.src, base.toString());
const host = url.host.split(":")[0]; const host = url.host.split(":")[0];
if ( if (
window.location.protocol === "https:" && window.location.protocol === "https:" &&
@ -326,7 +327,6 @@ export class GameScene extends DirtyScene {
this.onMapLoad(data); this.onMapLoad(data);
} }
this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.load as any).rexWebFont({ (this.load as any).rexWebFont({
custom: { custom: {
@ -555,7 +555,13 @@ 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.initCamera(); this.cameraManager = new CameraManager(
this,
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels },
waScaleManager
);
biggestAvailableAreaStore.recompute();
this.cameraManager.startFollow(this.CurrentPlayer);
this.animatedTiles.init(this.Map); this.animatedTiles.init(this.Map);
this.events.on("tileanimationupdate", () => (this.dirty = true)); this.events.on("tileanimationupdate", () => (this.dirty = true));
@ -596,7 +602,7 @@ export class GameScene extends DirtyScene {
// From now, this game scene will be notified of reposition events // From now, this game scene will be notified of reposition events
this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) =>
this.updateCameraOffset(box) this.cameraManager.updateCameraOffset(box)
); );
new GameMapPropertiesListener(this, this.gameMap).register(); new GameMapPropertiesListener(this, this.gameMap).register();
@ -649,7 +655,7 @@ export class GameScene extends DirtyScene {
* Initializes the connection to Pusher. * Initializes the connection to Pusher.
*/ */
private connect(): void { private connect(): void {
const camera = this.cameras.main; const camera = this.cameraManager.getCamera();
connectionManager connectionManager
.connectToRoomSocket( .connectToRoomSocket(
@ -671,7 +677,6 @@ export class GameScene extends DirtyScene {
this.connection = onConnect.connection; this.connection = onConnect.connection;
playersStore.connectToRoomConnection(this.connection); playersStore.connectToRoomConnection(this.connection);
userIsAdminStore.set(this.connection.hasTag("admin")); userIsAdminStore.set(this.connection.hasTag("admin"));
this.connection.onUserJoins((message: MessageUserJoined) => { this.connection.onUserJoins((message: MessageUserJoined) => {
@ -784,6 +789,42 @@ export class GameScene extends DirtyScene {
iframeListener.sendLeaveLayerEvent(layer.name); iframeListener.sendLeaveLayerEvent(layer.name);
}); });
}); });
this.gameMap.onEnterZone((zones) => {
for (const zone of zones) {
const focusable = zone.properties?.find((property) => property.name === "focusable");
if (focusable && focusable.value === true) {
const zoomMargin = zone.properties?.find((property) => property.name === "zoom_margin");
this.cameraManager.enterFocusMode(
zone,
zoomMargin ? Math.max(0, Number(zoomMargin.value)) : undefined
);
break;
}
}
zones.forEach((zone) => {
iframeListener.sendEnterZoneEvent(zone.name);
});
});
this.gameMap.onLeaveZone((zones) => {
for (const zone of zones) {
const focusable = zone.properties?.find((property) => property.name === "focusable");
if (focusable && focusable.value === true) {
this.cameraManager.leaveFocusMode(this.CurrentPlayer);
break;
}
}
zones.forEach((zone) => {
iframeListener.sendLeaveZoneEvent(zone.name);
});
});
// this.gameMap.onLeaveLayer((layers) => {
// layers.forEach((layer) => {
// iframeListener.sendLeaveLayerEvent(layer.name);
// });
// });
}); });
} }
@ -873,7 +914,8 @@ export class GameScene extends DirtyScene {
}; };
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER); const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE); let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) { if (message === undefined) {
message = "Press SPACE or touch here to enter Jitsi Meet room"; message = "Press SPACE or touch here to enter Jitsi Meet room";
@ -1202,6 +1244,7 @@ ${escapedMessage}
tags: this.connection ? this.connection.getAllTags() : [], tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables, variables: this.sharedVariablesManager.variables,
playerVariables: localUserStore.getAllUserProperties(), playerVariables: localUserStore.getAllUserProperties(),
userRoomToken: this.connection ? this.connection.userRoomToken : "",
}; };
}); });
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
@ -1427,6 +1470,7 @@ ${escapedMessage}
this.userInputManager.destroy(); this.userInputManager.destroy();
this.pinchManager?.destroy(); this.pinchManager?.destroy();
this.emoteManager.destroy(); this.emoteManager.destroy();
this.cameraManager.destroy();
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.emoteUnsubscribe(); this.emoteUnsubscribe();
this.emoteMenuUnsubscribe(); this.emoteMenuUnsubscribe();
@ -1459,7 +1503,7 @@ ${escapedMessage}
this.MapPlayers.remove(player); this.MapPlayers.remove(player);
}); });
this.MapPlayersByKey = new Map<number, RemotePlayer>(); this.MapPlayersByKey.clear();
} }
private getExitUrl(layer: ITiledMapLayer): string | undefined { private getExitUrl(layer: ITiledMapLayer): string | undefined {
@ -1517,13 +1561,6 @@ ${escapedMessage}
} }
} }
//todo: in a dedicated class/function?
initCamera() {
this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
this.cameras.main.startFollow(this.CurrentPlayer, true);
biggestAvailableAreaStore.recompute();
}
createCollisionWithPlayer() { createCollisionWithPlayer() {
//add collision layer //add collision layer
for (const phaserLayer of this.gameMap.phaserLayers) { for (const phaserLayer of this.gameMap.phaserLayers) {
@ -1917,23 +1954,6 @@ ${escapedMessage}
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
} }
/**
* Updates the offset of the character compared to the center of the screen according to the layout manager
* (tries to put the character in the center of the remaining space if there is a discussion going on.
*/
private updateCameraOffset(array: Box): void {
const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
// Let's put this in Game coordinates by applying the zoom level:
this.cameras.main.setFollowOffset(
((xCenter - game.offsetWidth / 2) * window.devicePixelRatio) / this.scale.zoom,
((yCenter - game.offsetHeight / 2) * window.devicePixelRatio) / this.scale.zoom
);
}
public startJitsi(roomName: string, jwt?: string): void { public startJitsi(roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties(); const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring( const jitsiConfig = this.safeParseJSONstring(
@ -2002,6 +2022,9 @@ ${escapedMessage}
} }
zoomByFactor(zoomFactor: number) { zoomByFactor(zoomFactor: number) {
if (this.cameraManager.isCameraLocked()) {
return;
}
waScaleManager.zoomModifier *= zoomFactor; waScaleManager.zoomModifier *= zoomFactor;
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
} }

View file

@ -1,7 +1,7 @@
import type { RoomConnection } from "../../Connexion/RoomConnection"; import type { RoomConnection } from "../../Connexion/RoomConnection";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import type { GameMap } from "./GameMap"; import type { GameMap } from "./GameMap";
import type { ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer } from "../Map/ITiledMap"; import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent"; import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent";

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