diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index ea67c3c1..45bcbfe0 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -10,7 +10,6 @@ on: pull_request: jobs: - continuous-integration-front: name: "Continuous Integration Front" @@ -46,6 +45,10 @@ jobs: run: ./templater.sh working-directory: "front" + - name: "Generate i18n files" + run: yarn run typesafe-i18n + working-directory: "front" + - name: "Build" run: yarn run build env: diff --git a/.github/workflows/end_to_end_tests.yml b/.github/workflows/end_to_end_tests.yml index ea9ba41c..f9dc832c 100644 --- a/.github/workflows/end_to_end_tests.yml +++ b/.github/workflows/end_to_end_tests.yml @@ -32,7 +32,7 @@ jobs: mode: start github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} ec2-image-id: ami-094dbcc53250a2480 - ec2-instance-type: t3.xlarge + ec2-instance-type: m5.2xlarge subnet-id: subnet-0ac40025f559df1bc security-group-id: sg-0e36e96e3b8ed2d64 #iam-role-name: my-role-name # optional, requires additional permissions diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml index 571a16e6..750ef224 100644 --- a/.github/workflows/push-to-npm.yml +++ b/.github/workflows/push-to-npm.yml @@ -43,6 +43,10 @@ jobs: run: ./templater.sh working-directory: "front" + - name: "Generate i18n files" + run: yarn run typesafe-i18n + working-directory: "front" + - name: "Build" run: yarn run build-typings env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3361333..2d3f5d0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,13 @@ # Contributing to WorkAdventure -Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to +Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to ask questions and how to work on something. ## Contributions we are seeking 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. ## Contributing external resources @@ -16,7 +16,7 @@ You can share your work on maps / articles / videos related to WorkAdventure on ## Developer documentation -Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/) +Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/) ## Using the issue tracker @@ -34,11 +34,11 @@ Finally, you can come and talk to the WorkAdventure core team... on WorkAdventur ## Pull requests -Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope +Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. -Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), -otherwise you risk spending a lot of time working on something that the project's developers might not want to merge +Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), +otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). @@ -54,7 +54,7 @@ $ yarn install $ yarn run prepare ``` -If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need +If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need to run code linting manually: ```console @@ -72,7 +72,7 @@ Nevertheless, if your code can be unit tested, please provide a unit test (we us 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. -* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference +* 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 @@ -90,8 +90,8 @@ $ 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. +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: @@ -107,3 +107,7 @@ $ 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 ``` + +### A bad wording or a missing language + +If you notice a translation error or missing language you can help us by following the [how to translate](docs/dev/how-to-translate.md) documentation. diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index e9fc0743..f571d6b2 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -15,6 +15,9 @@ export class DebugController { (async () => { const query = parse(req.getQuery()); + if (ADMIN_API_TOKEN === "") { + return res.writeStatus("401 Unauthorized").end("No token configured!"); + } if (query.token !== ADMIN_API_TOKEN) { return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); } diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index f7f0b084..f0f46a62 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -2,7 +2,7 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIM const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || ""; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; const JITSI_ISS = process.env.JITSI_ISS || ""; diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index c081f1b4..bf7ddd6e 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -1,11 +1,10 @@ import "jasmine"; -import {PositionNotifier} from "../src/Model/PositionNotifier"; -import {User, UserSocket} from "../src/Model/User"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionInterface} from "_Model/PositionInterface"; -import {ZoneSocket} from "../src/RoomManager"; - +import { PositionNotifier } from "../src/Model/PositionNotifier"; +import { User, UserSocket } from "../src/Model/User"; +import { Zone } from "_Model/Zone"; +import { Movable } from "_Model/Movable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { ZoneSocket } from "../src/RoomManager"; describe("PositionNotifier", () => { it("should receive notifications when player moves", () => { @@ -13,28 +12,59 @@ describe("PositionNotifier", () => { let moveTriggered = false; let leaveTriggered = false; - const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => { - enterTriggered = true; - }, (thing: Movable, position: PositionInterface) => { - moveTriggered = true; - }, (thing: Movable) => { - leaveTriggered = true; - }, () => {}, - () => {}); + const positionNotifier = new PositionNotifier( + 300, + 300, + (thing: Movable) => { + enterTriggered = true; + }, + (thing: Movable, position: PositionInterface) => { + moveTriggered = true; + }, + (thing: Movable) => { + leaveTriggered = true; + }, + () => {}, + () => {} + ); - const user1 = new User(1, 'test', '10.0.0.2', { - x: 500, - y: 500, - moving: false, - direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); + const user1 = new User( + 1, + "test", + "10.0.0.2", + { + x: 500, + y: 500, + moving: false, + direction: "down", + }, + false, + positionNotifier, + {} as UserSocket, + [], + null, + "foo", + [] + ); - const user2 = new User(2, 'test', '10.0.0.2', { - x: -9999, - y: -9999, - moving: false, - direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); + const user2 = new User( + 2, + "test", + "10.0.0.2", + { + x: -9999, + y: -9999, + moving: false, + direction: "down", + }, + false, + positionNotifier, + {} as UserSocket, + [], + null, + "foo", + [] + ); positionNotifier.addZoneListener({} as ZoneSocket, 0, 0); positionNotifier.addZoneListener({} as ZoneSocket, 0, 1); @@ -47,21 +77,21 @@ describe("PositionNotifier", () => { bottom: 500 });*/ - user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); + user2.setPosition({ x: 500, y: 500, direction: "down", moving: false }); expect(enterTriggered).toBe(true); expect(moveTriggered).toBe(false); enterTriggered = false; // Move inside the zone - user2.setPosition({x:501, y:500, direction: 'down', moving: false}); + user2.setPosition({ x: 501, y: 500, direction: "down", moving: false }); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(true); moveTriggered = false; // Move out of the zone in a zone that we don't track - user2.setPosition({x: 901, y: 500, direction: 'down', moving: false}); + user2.setPosition({ x: 901, y: 500, direction: "down", moving: false }); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(false); @@ -69,7 +99,7 @@ describe("PositionNotifier", () => { leaveTriggered = false; // Move back in - user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); + user2.setPosition({ x: 500, y: 500, direction: "down", moving: false }); expect(enterTriggered).toBe(true); expect(moveTriggered).toBe(false); expect(leaveTriggered).toBe(false); @@ -89,28 +119,59 @@ describe("PositionNotifier", () => { let moveTriggered = false; let leaveTriggered = false; - const positionNotifier = new PositionNotifier(300, 300, (thing: Movable, fromZone: Zone|null ) => { - enterTriggered = true; - }, (thing: Movable, position: PositionInterface) => { - moveTriggered = true; - }, (thing: Movable) => { - leaveTriggered = true; - }, () => {}, - () => {}); + const positionNotifier = new PositionNotifier( + 300, + 300, + (thing: Movable, fromZone: Zone | null) => { + enterTriggered = true; + }, + (thing: Movable, position: PositionInterface) => { + moveTriggered = true; + }, + (thing: Movable) => { + leaveTriggered = true; + }, + () => {}, + () => {} + ); - const user1 = new User(1, 'test', '10.0.0.2', { - x: 500, - y: 500, - moving: false, - direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); + const user1 = new User( + 1, + "test", + "10.0.0.2", + { + x: 500, + y: 500, + moving: false, + direction: "down", + }, + false, + positionNotifier, + {} as UserSocket, + [], + null, + "foo", + [] + ); - const user2 = new User(2, 'test', '10.0.0.2', { - x: 0, - y: 0, - moving: false, - direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); + const user2 = new User( + 2, + "test", + "10.0.0.2", + { + x: 0, + y: 0, + moving: false, + direction: "down", + }, + false, + positionNotifier, + {} as UserSocket, + [], + null, + "foo", + [] + ); const listener = {} as ZoneSocket; positionNotifier.addZoneListener(listener, 0, 0); @@ -126,14 +187,12 @@ describe("PositionNotifier", () => { positionNotifier.enter(user1); positionNotifier.enter(user2); - //expect(newUsers.length).toBe(2); expect(enterTriggered).toBe(true); enterTriggered = false; - //positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0}) - user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); + user2.setPosition({ x: 500, y: 500, direction: "down", moving: false }); expect(enterTriggered).toBe(true); expect(moveTriggered).toBe(false); @@ -184,4 +243,4 @@ describe("PositionNotifier", () => { enterTriggered = false; //expect(newUsers.length).toBe(2); }); -}) +}); diff --git a/back/yarn.lock b/back/yarn.lock index efd97ad8..04c6929b 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -935,9 +935,9 @@ flatted@^3.1.0: integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== follow-redirects@^1.14.0: - version "1.14.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" - integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== fs-minipass@^2.0.0: version "2.1.0" @@ -1504,9 +1504,9 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= node-fetch@^2.6.5: - version "2.6.6" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" - integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 0bbda264..4012b186 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -83,7 +83,8 @@ "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", - "START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json" + "START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json", + "ICON_URL": "//icon-"+url, } }, "uploader": { @@ -109,7 +110,15 @@ "redis": { "image": "redis:6", "ports": [6379] - } + }, + "iconserver": { + "image": "matthiasluedtke/iconserver:v3.13.0", + "host": { + "url": "icon-"+url, + "containerPort": 8080, + }, + "ports": [8080] + }, }, "config": { k8sextension(k8sConf):: @@ -210,6 +219,16 @@ } } }, + iconserver+: { + ingress+: { + spec+: { + tls+: [{ + hosts: ["icon-"+url], + secretName: "certificate-tls" + }] + } + } + }, } } } diff --git a/docker-compose.yaml b/docker-compose.yaml index 17e04f7c..8489b336 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -73,7 +73,7 @@ services: DEBUG: "socket:*" STARTUP_COMMAND_1: yarn install # wait for files generated by "messages" container to exists - STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done + STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_KEY: yourSecretKey ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" @@ -132,7 +132,7 @@ services: DEBUG: "*" STARTUP_COMMAND_1: yarn install # wait for files generated by "messages" container to exists - STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done + STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done SECRET_KEY: yourSecretKey SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" ALLOW_ARTILLERY: "true" diff --git a/docs/dev/how-to-translate.md b/docs/dev/how-to-translate.md new file mode 100644 index 00000000..b72b045a --- /dev/null +++ b/docs/dev/how-to-translate.md @@ -0,0 +1,76 @@ +# How to translate WorkAdventure + +We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation. + +## Add a new language + +It is very easy to add a new language! + +First, in the `front/src/i18n` folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)). + +In the previously created folder, add a file named index.ts with the following content containing your language information (french from France in this example): + +```ts +import type { Translation } from "../i18n-types"; + +const fr_FR: Translation = { + ...en_US, + language: "Français", + country: "France", +}; + +export default fr_FR; +``` + +## Add a new key + +### Add a simple key + +The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object. + +Please use kamelcase to name your keys! + +Example: + +```ts +{ + messages: { + coffeMachine: { + start: "Coffe machine has been started!"; + } + } +} +``` + +In the code you can translate using `$LL`: + +```ts +import LL from "../../i18n/i18n-svelte"; + +console.log($LL.messages.coffeMachine.start()); +``` + +### Add a key with parameters + +You can also use parameters to make the translation dynamic. +Use the tag { [parameter name] } to apply your parameters in the translations + +Example: + +```ts +{ + messages: { + coffeMachine: { + playerStart: "{ playerName } started the coffee machine!"; + } + } +} +``` + +In the code you can use it like this: + +```ts +$LL.messages.coffeMachine.playerStart.start({ + playerName: "John", +}); +``` diff --git a/docs/maps/api-camera.md b/docs/maps/api-camera.md index cb1fe72d..f0974ad6 100644 --- a/docs/maps/api-camera.md +++ b/docs/maps/api-camera.md @@ -1,6 +1,32 @@ {.section-title.accent.text-primary} + # API Camera functions Reference +### Start following player + +```javascript +WA.camera.followPlayer(smooth: boolean): void +``` +Set camera to follow the player. Set `smooth` to true for smooth transition. + +### Set spot for camera to look at + +```javascript +WA.camera.set( + x: number, + y: number, + width?: number, + height?: number, + lock: boolean = false, + smooth: boolean = false, +): void +``` + +Set camera to look at given spot. +Setting `width` and `height` will adjust zoom. +Set `lock` to true to lock camera in this position. +Set `smooth` to true for smooth transition. + ### Listen to camera updates ``` @@ -21,4 +47,4 @@ Example : ```javascript const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView)); //later... -subscription.unsubscribe(); \ No newline at end of file +subscription.unsubscribe(); diff --git a/docs/maps/api-nav.md b/docs/maps/api-nav.md index 47ee416e..2743d1ad 100644 --- a/docs/maps/api-nav.md +++ b/docs/maps/api-nav.md @@ -52,17 +52,17 @@ WA.nav.goToRoom("/_/global/.json#start-layer-2") ### Opening/closing web page in Co-Websites ``` -WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number = 0): Promise +WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number, closable: boolean, lazy: boolean): Promise ``` -Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open. -You can have only 5 co-wbesites open simultaneously. +Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy +it's to add the cowebsite but don't load it. Example: ```javascript const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/'); -const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1); +const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1, true, true); // ... coWebsite.close(); ``` diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index 58d5701a..c3aa808a 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -36,6 +36,23 @@ WA.onInit().then(() => { }) ``` +### Get the player language + +``` +WA.player.language: string; +``` + +The current language of player is available from the `WA.player.language` property. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.player.language` + +```typescript +WA.onInit().then(() => { + console.log('Player language: ', WA.player.language); +}) +``` + ### Get the tags of the player ``` @@ -58,6 +75,27 @@ WA.onInit().then(() => { }) ``` +### Get the position of the player +``` +WA.player.getPosition(): Promise +``` +The player's current position is available using the `WA.player.getPosition()` function. + +`Position` has the following attributes : +* **x (number) :** The coordinate x of the current player's position. +* **y (number) :** The coordinate y of the current player's position. + + +{.alert.alert-info} +You need to wait for the end of the initialization before calling `WA.player.getPosition()` + +```typescript +WA.onInit().then(() => { + console.log('Position: ', WA.player.getPosition()); +}) +``` + + ### Get the user-room token of the player ``` @@ -152,6 +190,37 @@ Example: WA.player.state.toto //will retrieve the variable ``` +### Move player to position +```typescript +WA.player.moveTo(x: number, y: number, speed?: number): Promise<{ x: number, y: number, cancelled: boolean }>; +``` +Player will try to find shortest path to the destination point and proceed to move there. +```typescript +// Let's move player to x: 250 y: 250 with speed of 10 +WA.player.moveTo(250, 250, 10); +``` +You can also chain movement like this: +```typescript +// Player will move to the next point after reaching first one +await WA.player.moveTo(250, 250, 10); +await WA.player.moveTo(500, 0, 10); +``` +Or like this: +```typescript +// Player will move to the next point after reaching first one or stop if the movement was cancelled +WA.player.moveTo(250, 250, 10).then((result) => { + if (!result.cancelled) { + WA.player.moveTo(500, 0, 10); + } +}); +``` +It is possible to get the information about current player's position on stop and if the movement was interrupted +```typescript +// Result will store x and y of Player at the moment of movement's end and information if the movement was interrupted +const result = await WA.player.moveTo(250, 250, 10); +// result: { x: number, y: number, cancelled: boolean } +``` + ### Set the outline color of the player ``` WA.player.setOutlineColor(red: number, green: number, blue: number): Promise; diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 7d438a1f..fa315abf 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -8,6 +8,7 @@ If you use group layers in your map, to reference a layer in a group you will ne together. Example : +
@@ -16,10 +17,10 @@ Example : The name of the layers of this map are : -* `entries/start` -* `bottom/ground/under` -* `bottom/build/carpet` -* `wall` +- `entries/start` +- `bottom/ground/under` +- `bottom/build/carpet` +- `wall` ### Detecting when the user enters/leaves a layer @@ -30,17 +31,18 @@ WA.room.onLeaveLayer(name: string): Subscription Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer. -* **name**: the name of the layer who as defined in Tiled. +- **name**: the name of the layer who as defined in Tiled. Example: -```javascript -WA.room.onEnterLayer('myLayer').subscribe(() => { - WA.chat.sendChatMessage("Hello!", 'Mr Robot'); +```ts +const myLayerSubscriber = WA.room.onEnterLayer("myLayer").subscribe(() => { + WA.chat.sendChatMessage("Hello!", "Mr Robot"); }); -WA.room.onLeaveLayer('myLayer').subscribe(() => { - WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); +WA.room.onLeaveLayer("myLayer").subscribe(() => { + WA.chat.sendChatMessage("Goodbye!", "Mr Robot"); + myLayerSubscriber.unsubscribe(); }); ``` @@ -56,10 +58,10 @@ layer in that group layer. Example : -```javascript -WA.room.showLayer('bottom'); +```ts +WA.room.showLayer("bottom"); //... -WA.room.hideLayer('bottom'); +WA.room.hideLayer("bottom"); ``` ### Set/Create properties in a layer @@ -76,8 +78,8 @@ To unset a property from a layer, use `setProperty` with `propertyValue` set to Example : -```javascript -WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); +```ts +WA.room.setProperty("wikiLayer", "openWebsite", "https://www.wikipedia.org/"); ``` ### Get the room id @@ -92,9 +94,9 @@ The ID of the current room is available from the `WA.room.id` property. ```typescript WA.onInit().then(() => { - console.log('Room id: ', WA.room.id); + console.log("Room id: ", WA.room.id); // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" -}) +}); ``` ### Get the map URL @@ -109,9 +111,9 @@ The URL of the map is available from the `WA.room.mapURL` property. ```typescript WA.onInit().then(() => { - console.log('Map URL: ', WA.room.mapURL); + console.log("Map URL: ", WA.room.mapURL); // Will output something like: 'https://mymap.org/map.json" -}) +}); ``` ### Getting map data @@ -122,7 +124,7 @@ WA.room.getTiledMap(): Promise Returns a promise that resolves to the JSON map file. -```javascript +```ts const map = await WA.room.getTiledMap(); console.log("Map generated with Tiled version ", map.tiledversion); ``` @@ -140,6 +142,7 @@ WA.room.setTiles(tiles: TileDescriptor[]): void Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. If `tile` is a string, it's not the id of the tile but the value of the property `name`. +
@@ -148,10 +151,10 @@ If `tile` is a string, it's not the id of the tile but the value of the property `TileDescriptor` has the following attributes : -* **x (number) :** The coordinate x of the tile that you want to replace. -* **y (number) :** The coordinate y of the tile that you want to replace. -* **tile (number | string) :** The id of the tile that will be placed in the map. -* **layer (string) :** The name of the layer where the tile will be placed. +- **x (number) :** The coordinate x of the tile that you want to replace. +- **y (number) :** The coordinate y of the tile that you want to replace. +- **tile (number | string) :** The id of the tile that will be placed in the map. +- **layer (string) :** The name of the layer where the tile will be placed. **Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor. @@ -160,12 +163,12 @@ Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`. Example : -```javascript +```ts WA.room.setTiles([ - { x: 6, y: 4, tile: 'blue', layer: 'setTiles' }, - { x: 7, y: 4, tile: 109, layer: 'setTiles' }, - { x: 8, y: 4, tile: 109, layer: 'setTiles' }, - { x: 9, y: 4, tile: 'blue', layer: 'setTiles' } + { x: 6, y: 4, tile: "blue", layer: "setTiles" }, + { x: 7, y: 4, tile: 109, layer: "setTiles" }, + { x: 8, y: 4, tile: 109, layer: "setTiles" }, + { x: 9, y: 4, tile: "blue", layer: "setTiles" }, ]); ``` @@ -179,10 +182,10 @@ Load a tileset in JSON format from an url and return the id of the first tile of You can create a tileset file in Tile Editor. -```javascript +```ts WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { - WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: 'bottom' }]); -}) + WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: "bottom" }]); +}); ``` ## Embedding websites in a map @@ -199,10 +202,10 @@ WA.room.website.get(objectName: string): Promise You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of an `EmbeddedWebsite` instance. -```javascript +```ts // Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) -const website = await WA.room.website.get('my_website'); -website.url = 'https://example.com'; +const website = await WA.room.website.get("my_website"); +website.url = "https://example.com"; website.visible = true; ``` @@ -231,7 +234,7 @@ interface CreateEmbeddedWebsiteEvent { You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns an `EmbeddedWebsite` instance. -```javascript +```ts // Create a new website object const website = WA.room.website.create({ name: "my_website", @@ -269,10 +272,10 @@ class EmbeddedWebsite { visible: boolean; allow: string; allowApi: boolean; - x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin - y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin - width: number; // In "game" pixels - height: number; // In "game" pixels + x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin + y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin + width: number; // In "game" pixels + height: number; // In "game" pixels origin: "player" | "map"; scale: number; } @@ -282,4 +285,3 @@ When you modify a property of an `EmbeddedWebsite` instance, the iframe is autom {.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users. - diff --git a/docs/maps/entry-exit.md b/docs/maps/entry-exit.md index 6f98af93..2040beb3 100644 --- a/docs/maps/entry-exit.md +++ b/docs/maps/entry-exit.md @@ -65,3 +65,24 @@ How to use entry point : * To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`". * You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL) + +## Defining destination point with moveTo parameter + +We are able to direct a Woka to the desired place immediately after spawn. To make users spawn on an entry point and then, walk automatically to a meeting room, simply add `moveTo` as an additional parameter of URL: + +*Use default entry point* +``` +.../my_map.json#&moveTo=exit +``` +*Define entry point and moveTo parameter like this...* +``` +.../my_map.json#start&moveTo=meeting-room +``` +*...or like this* +``` +.../my_map.json#moveTo=meeting-room&start +``` + +For this to work, moveTo must be equal to the layer name of interest. This layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected. + +![](images/moveTo-layer-example.png) \ No newline at end of file diff --git a/docs/maps/images/moveTo-layer-example.png b/docs/maps/images/moveTo-layer-example.png new file mode 100644 index 00000000..12e8a4ad Binary files /dev/null and b/docs/maps/images/moveTo-layer-example.png differ diff --git a/front/.eslintrc.js b/front/.eslintrc.js index ed94b3b2..fa57ebf4 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking" + "plugin:@typescript-eslint/recommended-requiring-type-checking", ], "globals": { "Atomics": "readonly", @@ -23,7 +23,7 @@ module.exports = { }, "plugins": [ "@typescript-eslint", - "svelte3" + "svelte3", ], "overrides": [ { @@ -33,6 +33,7 @@ module.exports = { ], "rules": { "no-unused-vars": "off", + "eol-last": ["error", "always"], "@typescript-eslint/no-explicit-any": "error", "no-throw-literal": "error", // TODO: remove those ignored rules and write a stronger code! diff --git a/front/.gitignore b/front/.gitignore index 1842041a..d76848c8 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -6,6 +6,7 @@ /dist/main.*.css.map /dist/tests/ /yarn-error.log +/package-lock.json /dist/webpack.config.js /dist/webpack.config.js.map /dist/src diff --git a/front/.prettierignore b/front/.prettierignore index 26de759f..8d8c68de 100644 --- a/front/.prettierignore +++ b/front/.prettierignore @@ -1,2 +1,5 @@ src/Messages/generated src/Messages/JsonMessages +src/i18n/i18n-svelte.ts +src/i18n/i18n-types.ts +src/i18n/i18n-util.ts diff --git a/front/.typesafe-i18n.json b/front/.typesafe-i18n.json new file mode 100644 index 00000000..0cecbe32 --- /dev/null +++ b/front/.typesafe-i18n.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json", + "baseLocale": "en-US", + "adapter": "svelte" +} \ No newline at end of file diff --git a/front/Dockerfile b/front/Dockerfile index 49cf6046..14914ebf 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,23 +1,35 @@ -FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder -WORKDIR /usr/src +FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder + +WORKDIR /usr/src/messages COPY messages . RUN yarn install && yarn ts-proto -# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL -FROM thecodingmachine/nodejs:14-apache +WORKDIR /usr/src/front +COPY front . -COPY --chown=docker:docker front . -COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated -RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts -COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages +# move messages to front +RUN cp -r ../messages/ts-proto-generated/protos/* src/Messages/ts-proto-generated +RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' src/Messages/ts-proto-generated/messages.ts +RUN cp -r ../messages/JsonMessages/* src/Messages/JsonMessages + +RUN yarn install && yarn run typesafe-i18n && yarn build # Removing the iframe.html file from the final image as this adds a XSS attack. # iframe.html is only in dev mode to circumvent a limitation RUN rm dist/iframe.html -RUN yarn install +FROM thecodingmachine/nodejs:14-apache + +COPY --from=builder --chown=docker:docker /usr/src/front/dist dist +COPY front/templater.sh . + +USER root +RUN DEBIAN_FRONTEND=noninteractive apt-get update \ + && apt-get install -y \ + gettext-base \ + && rm -rf /var/lib/apt/lists/* +USER docker -ENV NODE_ENV=production ENV STARTUP_COMMAND_0="./templater.sh" -ENV STARTUP_COMMAND_1="yarn run build" +ENV STARTUP_COMMAND_1="envsubst < dist/env-config.template.js > dist/env-config.js" ENV APACHE_DOCUMENT_ROOT=dist/ diff --git a/front/dist/.gitignore b/front/dist/.gitignore index 785f2eb9..bc766e57 100644 --- a/front/dist/.gitignore +++ b/front/dist/.gitignore @@ -1,4 +1,6 @@ index.html -index.tmpl.html.tmp /js/ +/fonts/ style.*.css +!env-config.template.js +*.png diff --git a/front/dist/env-config.template.js b/front/dist/env-config.template.js new file mode 100644 index 00000000..e672d7aa --- /dev/null +++ b/front/dist/env-config.template.js @@ -0,0 +1,27 @@ +window.env = { + SKIP_RENDER_OPTIMIZATIONS: '${SKIP_RENDER_OPTIMIZATIONS}', + DISABLE_NOTIFICATIONS: '${DISABLE_NOTIFICATIONS}', + PUSHER_URL: '${PUSHER_URL}', + UPLOADER_URL: '${UPLOADER_URL}', + ADMIN_URL: '${ADMIN_URL}', + CONTACT_URL: '${CONTACT_URL}', + PROFILE_URL: '${PROFILE_URL}', + ICON_URL: '${ICON_URL}', + DEBUG_MODE: '${DEBUG_MODE}', + STUN_SERVER: '${STUN_SERVER}', + TURN_SERVER: '${TURN_SERVER}', + TURN_USER: '${TURN_USER}', + TURN_PASSWORD: '${TURN_PASSWORD}', + JITSI_URL: '${JITSI_URL}', + JITSI_PRIVATE_MODE: '${JITSI_PRIVATE_MODE}', + START_ROOM_URL: '${START_ROOM_URL}', + MAX_USERNAME_LENGTH: '${MAX_USERNAME_LENGTH}', + MAX_PER_GROUP: '${MAX_PER_GROUP}', + DISPLAY_TERMS_OF_USE: '${DISPLAY_TERMS_OF_USE}', + POSTHOG_API_KEY: '${POSTHOG_API_KEY}', + POSTHOG_URL: '${POSTHOG_URL}', + NODE_ENV: '${NODE_ENV}', + DISABLE_ANONYMOUS: '${DISABLE_ANONYMOUS}', + OPID_LOGIN_SCREEN_PROVIDER: '${OPID_LOGIN_SCREEN_PROVIDER}', + FALLBACK_LOCALE: '${FALLBACK_LOCALE}', +}; diff --git a/front/dist/index.ejs b/front/dist/index.ejs new file mode 100644 index 00000000..07732877 --- /dev/null +++ b/front/dist/index.ejs @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WorkAdventure + + +
+ +
+
+
+
+ +
+ +
+
+
+
+ + diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html deleted file mode 100644 index 3b43a5ef..00000000 --- a/front/dist/index.tmpl.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WorkAdventure - - -
- -
-
-
-
-
- - -
-
-
- -
-
-
-
- -
-
- - -
-
-
- -
-
-
- -
-
- - -
-
-
- -
-
-
- -
-
- - -
-
-
-
-
-
-
-
-
- - -
-
- -
-
- -
-
- - - - diff --git a/front/dist/resources/logos/cowebsite-swipe.svg b/front/dist/resources/logos/cowebsite-swipe.svg new file mode 100644 index 00000000..1d4f9ebc --- /dev/null +++ b/front/dist/resources/logos/cowebsite-swipe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/translations/.gitignore b/front/dist/resources/translations/.gitignore new file mode 100644 index 00000000..a6c57f5f --- /dev/null +++ b/front/dist/resources/translations/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/front/package.json b/front/package.json index 935c254f..86308a89 100644 --- a/front/package.json +++ b/front/package.json @@ -15,6 +15,7 @@ "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "css-loader": "^5.2.4", + "css-minimizer-webpack-plugin": "^3.3.1", "eslint": "^8.4.1", "eslint-plugin-svelte3": "^3.2.1", "fork-ts-checker-webpack-plugin": "^6.5.0", @@ -46,8 +47,10 @@ "@types/simple-peer": "^9.11.1", "@types/socket.io-client": "^1.4.32", "axios": "^0.21.2", + "cancelable-promise": "^4.2.1", "cross-env": "^7.0.3", "deep-copy-ts": "^0.5.0", + "easystarjs": "^0.4.4", "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", "phaser": "^3.54.0", @@ -63,10 +66,12 @@ "socket.io-client": "^2.3.0", "standardized-audio-context": "^25.2.4", "ts-proto": "^1.96.0", - "uuidv4": "^6.2.10" + "typesafe-i18n": "^2.59.0", + "uuidv4": "^6.2.10", + "zod": "^3.11.6" }, "scripts": { - "start": "run-p templater serve svelte-check-watch", + "start": "run-p templater serve svelte-check-watch typesafe-i18n", "templater": "cross-env ./templater.sh", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", @@ -78,7 +83,8 @@ "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\"", "pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'", - "pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'" + "pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'", + "typesafe-i18n": "typesafe-i18n --no-watch" }, "lint-staged": { "*.svelte": [ diff --git a/front/src/Api/Events/CameraFollowPlayerEvent.ts b/front/src/Api/Events/CameraFollowPlayerEvent.ts new file mode 100644 index 00000000..cf34e7fc --- /dev/null +++ b/front/src/Api/Events/CameraFollowPlayerEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isCameraFollowPlayerEvent = new tg.IsInterface() + .withProperties({ + smooth: tg.isBoolean, + }) + .get(); +/** + * A message sent from the iFrame to the game to make the camera follow player. + */ +export type CameraFollowPlayerEvent = tg.GuardedType; diff --git a/front/src/Api/Events/CameraSetEvent.ts b/front/src/Api/Events/CameraSetEvent.ts new file mode 100644 index 00000000..a3da7c62 --- /dev/null +++ b/front/src/Api/Events/CameraSetEvent.ts @@ -0,0 +1,16 @@ +import * as tg from "generic-type-guard"; + +export const isCameraSetEvent = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + width: tg.isOptional(tg.isNumber), + height: tg.isOptional(tg.isNumber), + lock: tg.isBoolean, + smooth: tg.isBoolean, + }) + .get(); +/** + * A message sent from the iFrame to the game to change the camera position. + */ +export type CameraSetEvent = tg.GuardedType; diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 9755ba9e..80c07e5a 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -5,12 +5,13 @@ export const isGameStateEvent = new tg.IsInterface() roomId: tg.isString, mapUrl: tg.isString, nickname: tg.isString, + language: tg.isUnion(tg.isString, tg.isUndefined), uuid: tg.isUnion(tg.isString, tg.isUndefined), startLayerName: tg.isUnion(tg.isString, tg.isNull), tags: tg.isArray(tg.isString), variables: tg.isObject, - userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), playerVariables: tg.isObject, + userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), }) .get(); /** diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 8fb488dc..e56699a7 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -28,10 +28,14 @@ import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent"; -import type { ChangeZoneEvent } from "./ChangeZoneEvent"; -import { isColorEvent } from "./ColorEvent"; import { isPlayerPosition } from "./PlayerPosition"; import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent"; +import type { ChangeZoneEvent } from "./ChangeZoneEvent"; +import type { CameraSetEvent } from "./CameraSetEvent"; +import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent"; +import { isColorEvent } from "./ColorEvent"; +import { isMovePlayerToEventConfig } from "./MovePlayerToEvent"; +import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -43,6 +47,8 @@ export interface TypedMessageEvent extends MessageEvent { export type IframeEventMap = { loadPage: LoadPageEvent; chat: ChatEvent; + cameraFollowPlayer: CameraFollowPlayerEvent; + cameraSet: CameraSetEvent; openPopup: OpenPopupEvent; closePopup: ClosePopupEvent; openTab: OpenTabEvent; @@ -169,6 +175,10 @@ export const iframeQueryMapTypeGuards = { query: tg.isUndefined, answer: isPlayerPosition, }, + movePlayerTo: { + query: isMovePlayerToEventConfig, + answer: isMovePlayerToEventAnswer, + }, }; type GuardedType = T extends (x: unknown) => x is infer T ? T : never; diff --git a/front/src/Api/Events/MovePlayerToEvent.ts b/front/src/Api/Events/MovePlayerToEvent.ts new file mode 100644 index 00000000..462e2f43 --- /dev/null +++ b/front/src/Api/Events/MovePlayerToEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isMovePlayerToEventConfig = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + speed: tg.isOptional(tg.isNumber), + }) + .get(); + +export type MovePlayerToEvent = tg.GuardedType; diff --git a/front/src/Api/Events/MovePlayerToEventAnswer.ts b/front/src/Api/Events/MovePlayerToEventAnswer.ts new file mode 100644 index 00000000..67d2f9ae --- /dev/null +++ b/front/src/Api/Events/MovePlayerToEventAnswer.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isMovePlayerToEventAnswer = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + cancelled: tg.isBoolean, + }) + .get(); + +export type MovePlayerToEventAnswer = tg.GuardedType; diff --git a/front/src/Api/Events/OpenCoWebsiteEvent.ts b/front/src/Api/Events/OpenCoWebsiteEvent.ts index 514fd110..51a17763 100644 --- a/front/src/Api/Events/OpenCoWebsiteEvent.ts +++ b/front/src/Api/Events/OpenCoWebsiteEvent.ts @@ -6,13 +6,14 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface() allowApi: tg.isOptional(tg.isBoolean), allowPolicy: tg.isOptional(tg.isString), position: tg.isOptional(tg.isNumber), + closable: tg.isOptional(tg.isBoolean), + lazy: tg.isOptional(tg.isBoolean), }) .get(); export const isCoWebsite = new tg.IsInterface() .withProperties({ id: tg.isString, - position: tg.isNumber, }) .get(); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 85e1373b..65ab1303 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -8,7 +8,6 @@ import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import { scriptUtils } from "./ScriptUtils"; import { isGoToPageEvent } from "./Events/GoToPageEvent"; -import { isCloseCoWebsite, CloseCoWebsiteEvent } from "./Events/CloseCoWebsiteEvent"; import { IframeErrorAnswerEvent, IframeQueryMap, @@ -33,6 +32,8 @@ import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Store import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; +import { CameraSetEvent, isCameraSetEvent } from "./Events/CameraSetEvent"; +import { CameraFollowPlayerEvent, isCameraFollowPlayerEvent } from "./Events/CameraFollowPlayerEvent"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -56,6 +57,12 @@ class IframeListener { private readonly _disablePlayerControlStream: Subject = new Subject(); public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable(); + private readonly _cameraSetStream: Subject = new Subject(); + public readonly cameraSetStream = this._cameraSetStream.asObservable(); + + private readonly _cameraFollowPlayerStream: Subject = new Subject(); + public readonly cameraFollowPlayerStream = this._cameraFollowPlayerStream.asObservable(); + private readonly _enablePlayerControlStream: Subject = new Subject(); public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable(); @@ -202,6 +209,10 @@ class IframeListener { this._hideLayerStream.next(payload.data); } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { this._setPropertyStream.next(payload.data); + } else if (payload.type === "cameraSet" && isCameraSetEvent(payload.data)) { + this._cameraSetStream.next(payload.data); + } else if (payload.type === "cameraFollowPlayer" && isCameraFollowPlayerEvent(payload.data)) { + this._cameraFollowPlayerStream.next(payload.data); } else if (payload.type === "chat" && isChatEvent(payload.data)) { scriptUtils.sendAnonymousChat(payload.data); } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts index 10a80c92..f0a0625a 100644 --- a/front/src/Api/ScriptUtils.ts +++ b/front/src/Api/ScriptUtils.ts @@ -1,4 +1,3 @@ -import { coWebsiteManager, CoWebsite } from "../WebRtc/CoWebsiteManager"; import { playersStore } from "../Stores/PlayersStore"; import { chatMessagesStore } from "../Stores/ChatStore"; import type { ChatEvent } from "./Events/ChatEvent"; diff --git a/front/src/Api/iframe/camera.ts b/front/src/Api/iframe/camera.ts index a832290e..38199e0d 100644 --- a/front/src/Api/iframe/camera.ts +++ b/front/src/Api/iframe/camera.ts @@ -17,6 +17,27 @@ export class WorkAdventureCameraCommands extends IframeApiContribution { sendToWorkadventure({ type: "onCameraUpdate", diff --git a/front/src/Api/iframe/chat.ts b/front/src/Api/iframe/chat.ts index 5797df5a..c642b5a8 100644 --- a/front/src/Api/iframe/chat.ts +++ b/front/src/Api/iframe/chat.ts @@ -1,4 +1,3 @@ -import type { ChatEvent } from "../Events/ChatEvent"; import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent"; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index 206961bf..d5362b4b 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -1,7 +1,7 @@ import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution"; export class CoWebsite { - constructor(private readonly id: string, public readonly position: number) {} + constructor(private readonly id: string) {} close() { return queryWorkadventure({ @@ -41,7 +41,14 @@ export class WorkadventureNavigationCommands extends IframeApiContribution { + async openCoWebSite( + url: string, + allowApi?: boolean, + allowPolicy?: string, + position?: number, + closable?: boolean, + lazy?: boolean + ): Promise { const result = await queryWorkadventure({ type: "openCoWebsite", data: { @@ -49,9 +56,11 @@ export class WorkadventureNavigationCommands extends IframeApiContribution { @@ -59,7 +68,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution new CoWebsite(cowebsiteEvent.id, cowebsiteEvent.position)); + return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id)); } /** diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index 0c71ae33..48de68d2 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -13,6 +13,12 @@ export const setPlayerName = (name: string) => { playerName = name; }; +let playerLanguage: string | undefined; + +export const setPlayerLanguage = (language: string | undefined) => { + playerLanguage = language; +}; + let tags: string[] | undefined; export const setTags = (_tags: string[]) => { @@ -61,6 +67,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution { + return await queryWorkadventure({ + type: "movePlayerTo", + data: { x, y, speed }, + }); + } + get userRoomToken(): string | undefined { if (userRoomToken === undefined) { throw new Error( diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index a1277ed2..620884cf 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -1,166 +1,52 @@ -
- {#if $loginSceneVisibleStore} -
- -
- {/if} - {#if $selectCharacterSceneVisibleStore} -
- -
- {/if} - {#if $customCharacterSceneVisibleStore} -
- -
- {/if} - {#if $selectCompanionSceneVisibleStore} -
- -
- {/if} - {#if $enableCameraSceneVisibilityStore} -
- -
- {/if} - {#if $banMessageStore.length > 0} -
- -
- {:else if $textMessageStore.length > 0} -
- -
- {/if} - {#if $soundPlayingStore} -
- -
- {/if} - {#if $audioManagerVisibilityStore} -
- -
- {/if} - {#if $layoutManagerVisibilityStore} -
- -
- {/if} - {#if $showReportScreenStore !== userReportEmpty} -
- -
- {/if} - {#if $followStateStore !== "off" || $peerStore.size > 0} -
- -
- {/if} - {#if $menuIconVisiblilityStore} -
- -
- {/if} - {#if $menuVisiblilityStore} -
- -
- {/if} - {#if $emoteMenuStore} -
- -
- {/if} - {#if $gameOverlayVisibilityStore} -
- - - -
- {/if} - {#if $helpCameraSettingsVisibleStore} -
- -
- {/if} - {#if $showLimitRoomModalStore} -
- -
- {/if} - {#if $showShareLinkMapModalStore} -
- -
- {/if} - {#if $requestVisitCardsStore} - - {/if} - {#if $errorStore.length > 0} -
- -
- {/if} +{#if $errorStore.length > 0} +
+ +
+{:else if $loginSceneVisibleStore} +
+ +
+{:else if $selectCharacterSceneVisibleStore} +
+ +
+{:else if $customCharacterSceneVisibleStore} +
+ +
+{:else if $selectCompanionSceneVisibleStore} +
+ +
+{:else if $enableCameraSceneVisibilityStore} +
+ +
+{:else} + + {#if $chatVisibilityStore} {/if} - {#if $warningContainerStore} - - {/if} -
+{/if} diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index b62d8fbe..eec51572 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -5,6 +5,7 @@ import { get } from "svelte/store"; import type { Unsubscriber } from "svelte/store"; import { onDestroy, onMount } from "svelte"; + import LL from "../../i18n/i18n-svelte"; let HTMLAudioPlayer: HTMLAudioElement; let audioPlayerVolumeIcon: HTMLElement; @@ -144,7 +145,7 @@
@@ -156,13 +157,16 @@ diff --git a/front/src/Components/Chat/Chat.svelte b/front/src/Components/Chat/Chat.svelte index 6827dde4..cd9b90b5 100644 --- a/front/src/Components/Chat/Chat.svelte +++ b/front/src/Components/Chat/Chat.svelte @@ -5,6 +5,7 @@ import ChatElement from "./ChatElement.svelte"; import { afterUpdate, beforeUpdate, onMount } from "svelte"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; + import LL from "../../i18n/i18n-svelte"; let listDom: HTMLElement; let chatWindowElement: HTMLElement; @@ -42,10 +43,10 @@
@@ -86,7 +87,7 @@ {:else if reportActive} {:else} -

ERROR : There is no action selected.

+

{$LL.report.moderate.noSelect()}

{/if}
@@ -107,12 +108,16 @@ pointer-events: auto; background-color: #333333; color: whitesmoke; - - position: relative; + z-index: 650; + position: absolute; height: 70vh; width: 50vw; - top: 10vh; - margin: auto; + top: 4%; + + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; section.report-menu-title { display: grid; @@ -136,13 +141,4 @@ display: none; } } - - @media only screen and (max-width: 800px) { - div.report-menu-main { - top: 21vh; - height: 60vh; - width: 100vw; - font-size: 0.5em; - } - } diff --git a/front/src/Components/ReportMenu/ReportSubMenu.svelte b/front/src/Components/ReportMenu/ReportSubMenu.svelte index 379dd663..114d837a 100644 --- a/front/src/Components/ReportMenu/ReportSubMenu.svelte +++ b/front/src/Components/ReportMenu/ReportSubMenu.svelte @@ -1,6 +1,7 @@
-

Report

-

Send a report message to the administrators of this room. They may later ban this user.

+

{$LL.report.title()}

+

{$LL.report.content()}