diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 3bf00b99..e33346d0 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -199,4 +199,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - msg: Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re + msg: "Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re \nTests available at https://maps-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re/tests" diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml index 798e2530..fd247b11 100644 --- a/.github/workflows/push-to-npm.yml +++ b/.github/workflows/push-to-npm.yml @@ -2,6 +2,7 @@ name: Push @workadventure/iframe-api-typings to NPM on: release: types: [created] + push: jobs: build: runs-on: ubuntu-latest @@ -13,10 +14,6 @@ jobs: node-version: '14.x' registry-url: 'https://registry.npmjs.org' - - name: Edit tsconfig.json to add declarations - run: "sed -i 's/\"declaration\": false/\"declaration\": true/g' tsconfig.json" - working-directory: "front" - - name: Replace version number run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json' working-directory: "front/packages/iframe-api-typings" @@ -47,15 +44,18 @@ jobs: working-directory: "front" - name: "Build" - run: yarn run build + run: yarn run build-typings env: - API_URL: "localhost:8080" + PUSHER_URL: "//localhost:8080" working-directory: "front" # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. - name: Copy typings to package dir run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts + - name: Copy typings to package dir (2) + run: cp -R front/dist/src/Api front/packages/iframe-api-typings/Api + - name: Install dependencies in package run: yarn install working-directory: "front/packages/iframe-api-typings" @@ -65,3 +65,4 @@ jobs: working-directory: "front/packages/iframe-api-typings" env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + if: ${{ github.event_name == 'release' }} diff --git a/.gitignore b/.gitignore index 2cbf66b1..8fa69985 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ docker-compose.override.yaml maps/yarn.lock maps/dist/computer.js maps/dist/computer.js.map -/node_modules/ +node_modules +_ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f72510..46034e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,35 @@ - New scripting API features : - Use `WA.room.showLayer(): void` to show a layer - Use `WA.room.hideLayer(): void` to hide a layer - - Use `WA.room.setProperty() : void` to add or change existing property of a layer + - Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer - Use `WA.player.onPlayerMove(): void` to track the movement of the current player - - Use `WA.room.getCurrentUser(): Promise` to get the ID, name and tags of the current player + - Use `WA.player.getCurrentUser(): Promise` to get the ID, name and tags of the current player - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - Use `WA.ui.registerMenuCommand(): void` to add a custom menu + - Use `WA.room.setTiles(): void` to add, delete or change an array of tiles +- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. +- The text chat was redesigned to be prettier and to use more features : + - The chat is now persistent bewteen discussions and always accesible + - The chat now tracks incoming and outcoming users in your conversation + - The chat allows your to see the visit card of users + - You can close the chat window with the escape key +- Added a 'Enable notifications' button in the menu. +- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed: + - `/api/map`: now accepts a complete room URL instead of organization/world/room slugs + - `/api/ban`: new endpoint to report users + - as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing + +## Version 1.4.3 - 1.4.4 - 1.4.5 + +## Bugfixes + +- Fixing the generation of @workadventure/iframe-api-typings + +## Version 1.4.2 + +## Updates + +- A script in an iframe opened by another script can use the IFrame API. ## Version 1.4.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b85d0a98..8bbbc93e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ Before committing, be sure to install the "Prettier" precommit hook that will re In order to enable the "Prettier" precommit hook, at the root of the project, run: ```console -$ yarn run install +$ yarn install $ yarn run prepare ``` diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index b7f037fd..88287753 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -15,7 +15,7 @@ export class DebugController { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send("Invalid token sent!"); + return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); } return res diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 020f4c29..f2b736c6 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -5,8 +5,6 @@ import { PositionInterface } from "_Model/PositionInterface"; import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; import { PositionNotifier } from "./PositionNotifier"; import { Movable } from "_Model/Movable"; -import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; -import { arrayIntersect } from "../Services/ArrayHelper"; import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ZoneSocket } from "src/RoomManager"; @@ -15,12 +13,6 @@ import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; -export enum GameRoomPolicyTypes { - ANONYMOUS_POLICY = 1, - MEMBERS_ONLY_POLICY, - USE_TAGS_POLICY, -} - export class GameRoom { private readonly minDistance: number; private readonly groupRadius: number; @@ -37,15 +29,12 @@ export class GameRoom { private itemsState: Map = new Map(); private readonly positionNotifier: PositionNotifier; - public readonly roomId: string; - public readonly roomSlug: string; - public readonly worldSlug: string = ""; - public readonly organizationSlug: string = ""; + public readonly roomUrl: string; private versionNumber: number = 1; private nextUserId: number = 1; constructor( - roomId: string, + roomUrl: string, connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback, minDistance: number, @@ -55,16 +44,7 @@ export class GameRoom { onLeaves: LeavesCallback, onEmote: EmoteCallback ) { - this.roomId = roomId; - - if (isRoomAnonymous(roomId)) { - this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); - } else { - const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); - this.roomSlug = roomSlug; - this.organizationSlug = organizationSlug; - this.worldSlug = worldSlug; - } + this.roomUrl = roomUrl; this.users = new Map(); this.usersByUuid = new Map(); @@ -183,7 +163,7 @@ export class GameRoom { } else { const closestUser: User = closestItem; const group: Group = new Group( - this.roomId, + this.roomUrl, [user, closestUser], this.connectCallback, this.disconnectCallback, diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts deleted file mode 100644 index d1de8800..00000000 --- a/back/src/Model/RoomIdentifier.ts +++ /dev/null @@ -1,30 +0,0 @@ -//helper functions to parse room IDs - -export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith("_/")) { - return true; - } else if (roomID.startsWith("@/")) { - return false; - } else { - throw new Error("Incorrect room ID: " + roomID); - } -}; - -export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split("/"); - if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); - return idParts.slice(2).join("/"); -}; -export interface extractDataFromPrivateRoomIdResponse { - organizationSlug: string; - worldSlug: string; - roomSlug: string; -} -export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split("/"); - if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); - const organizationSlug = idParts[1]; - const worldSlug = idParts[2]; - const roomSlug = idParts[3]; - return { organizationSlug, worldSlug, roomSlug }; -}; diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index e61763cd..5e45c975 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -250,12 +250,12 @@ export class SocketManager { //user leave previous world room.leave(user); if (room.isEmpty()) { - this.rooms.delete(room.roomId); + this.rooms.delete(room.roomUrl); gaugeManager.decNbRoomGauge(); - debug('Room is empty. Deleting room "%s"', room.roomId); + debug('Room is empty. Deleting room "%s"', room.roomUrl); } } finally { - clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); + clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl); console.log("A user left"); } } @@ -308,6 +308,7 @@ export class SocketManager { throw new Error("clientUser.userId is not an integer " + thing.id); } userJoinedZoneMessage.setUserid(thing.id); + userJoinedZoneMessage.setUseruuid(thing.uuid); userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); @@ -425,7 +426,6 @@ export class SocketManager { // Let's send 2 messages: one to the user joining the group and one to the other user const webrtcStartMessage1 = new WebRtcStartMessage(); webrtcStartMessage1.setUserid(otherUser.id); - webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); if (TURN_STATIC_AUTH_SECRET !== "") { const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); @@ -436,14 +436,10 @@ export class SocketManager { const serverToClientMessage1 = new ServerToClientMessage(); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); - //if (!user.socket.disconnecting) { user.socket.write(serverToClientMessage1); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) - //} const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); - webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); if (TURN_STATIC_AUTH_SECRET !== "") { const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); @@ -454,10 +450,7 @@ export class SocketManager { const serverToClientMessage2 = new ServerToClientMessage(); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); - //if (!otherUser.socket.disconnecting) { otherUser.socket.write(serverToClientMessage2); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) - //} } } @@ -614,6 +607,7 @@ export class SocketManager { if (thing instanceof User) { const userJoinedMessage = new UserJoinedZoneMessage(); userJoinedMessage.setUserid(thing.id); + userJoinedMessage.setUseruuid(thing.uuid); userJoinedMessage.setName(thing.name); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); @@ -664,9 +658,9 @@ export class SocketManager { public leaveAdminRoom(room: GameRoom, admin: Admin) { room.adminLeave(admin); if (room.isEmpty()) { - this.rooms.delete(room.roomId); + this.rooms.delete(room.roomUrl); gaugeManager.decNbRoomGauge(); - debug('Room is empty. Deleting room "%s"', room.roomId); + debug('Room is empty. Deleting room "%s"', room.roomUrl); } } diff --git a/back/tests/RoomIdentifierTest.ts b/back/tests/RoomIdentifierTest.ts deleted file mode 100644 index c3817ff7..00000000 --- a/back/tests/RoomIdentifierTest.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier"; - -describe("RoomIdentifier", () => { - it("should flag public id as anonymous", () => { - expect(isRoomAnonymous('_/global/test')).toBe(true); - }); - it("should flag public id as not anonymous", () => { - expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false); - }); - it("should extract roomSlug from public ID", () => { - expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json'); - }); - it("should extract correct from private ID", () => { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor'); - expect(organizationSlug).toBe('afup'); - expect(worldSlug).toBe('afup2020'); - expect(roomSlug).toBe('1floor'); - }); -}) \ No newline at end of file diff --git a/docs/maps/api-nav.md b/docs/maps/api-nav.md index 29323632..f5721063 100644 --- a/docs/maps/api-nav.md +++ b/docs/maps/api-nav.md @@ -52,11 +52,11 @@ WA.nav.goToRoom("/_/global/.json#start-layer-2") ### Opening/closing a web page in an iFrame ``` -WA.nav.openCoWebSite(url: string): void +WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void WA.nav.closeCoWebSite(): void ``` -Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. +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). Example: @@ -65,4 +65,3 @@ WA.nav.openCoWebSite('https://www.wikipedia.org/'); // ... WA.nav.closeCoWebSite(); ``` - diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index f483731e..0efe2941 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -1,6 +1,7 @@ {.section-title.accent.text-primary} # API Player functions Reference + ### Listen to player movement ``` WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 30a11b2a..8c8205d8 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -9,4 +9,4 @@ - [Sound functions](api-sound.md) - [Controls functions](api-controls.md) -- [List of deprecated functions](api-deprecated.md) \ No newline at end of file +- [List of deprecated functions](api-deprecated.md) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index d8381cc6..8bc2b3d2 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -54,6 +54,7 @@ WA.room.showLayer(layerName : string): void WA.room.hideLayer(layerName : string) : void ``` These 2 methods can be used to show and hide a layer. +if `layerName` is the name of a group layer, show/hide all the layer in that group layer. Example : ```javascript @@ -70,45 +71,43 @@ WA.room.setProperty(layerName : string, propertyName : string, propertyValue : s Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. +Note : +To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`. + Example : ```javascript WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` -### Getting information on the current room +### Changing tiles ``` -WA.room.getCurrentRoom(): Promise +WA.room.setTiles(tiles: TileDescriptor[]): void ``` -Return a promise that resolves to a `Room` object with the following attributes : -* **id (string) :** ID of the current room -* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. -* **mapUrl (string) :** Url of the JSON map file -* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer +Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. -Example : +If `tile` is a string, it's not the id of the tile but the value of the property `name`. +
+
+ +
+
+ +`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. + +**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. + +Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`. + +Example : ```javascript -WA.room.getCurrentRoom((room) => { - if (room.id === '42') { - console.log(room.map); - window.open(room.mapUrl, '_blank'); - } -}) -``` - -### Getting information on the current user -``` -WA.player.getCurrentUser(): Promise -``` -Return a promise that resolves to a `User` object with the following attributes : -* **id (string) :** ID of the current user -* **nickName (string) :** name displayed above the current user -* **tags (string[]) :** list of all the tags of the current user - -Example : -```javascript -WA.room.getCurrentUser().then((user) => { - if (user.nickName === 'ABC') { - console.log(user.tags); - } -}) +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'} + ]); ``` diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index e8b2a161..d2fa3874 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -37,8 +37,7 @@
-
-
+
diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html index 26be2a1c..bb0a6e1e 100644 --- a/front/dist/resources/html/gameMenu.html +++ b/front/dist/resources/html/gameMenu.html @@ -57,6 +57,9 @@
+
+ +
diff --git a/front/dist/resources/logos/tcm_full.png b/front/dist/resources/logos/tcm_full.png deleted file mode 100644 index 3ea27990..00000000 Binary files a/front/dist/resources/logos/tcm_full.png and /dev/null differ diff --git a/front/dist/resources/logos/tcm_short.png b/front/dist/resources/logos/tcm_short.png deleted file mode 100644 index ed55c836..00000000 Binary files a/front/dist/resources/logos/tcm_short.png and /dev/null differ diff --git a/front/dist/resources/service-worker.js b/front/dist/resources/service-worker.js new file mode 100644 index 00000000..e496f7fc --- /dev/null +++ b/front/dist/resources/service-worker.js @@ -0,0 +1,53 @@ +let CACHE_NAME = 'workavdenture-cache-v1'; +let urlsToCache = [ + '/' +]; + +self.addEventListener('install', function(event) { + // Perform install steps + event.waitUntil( + caches.open(CACHE_NAME) + .then(function(cache) { + console.log('Opened cache'); + return cache.addAll(urlsToCache); + }) + ); +}); + +self.addEventListener('fetch', function(event) { + event.respondWith( + caches.match(event.request) + .then(function(response) { + // Cache hit - return response + if (response) { + return response; + } + + return fetch(event.request).then( + function(response) { + // Check if we received a valid response + if(!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + + // IMPORTANT: Clone the response. A response is a stream + // and because we want the browser to consume the response + // as well as the cache consuming the response, we need + // to clone it so we have two streams. + var responseToCache = response.clone(); + + caches.open(CACHE_NAME) + .then(function(cache) { + cache.put(event.request, responseToCache); + }); + + return response; + } + ); + }) + ); +}); + +self.addEventListener('activate', function(event) { + //TODO activate service worker +}); \ No newline at end of file diff --git a/front/dist/static/images/favicons/android-icon-144x144.png b/front/dist/static/images/favicons/android-icon-144x144.png index b0463804..59a7c4ce 100644 Binary files a/front/dist/static/images/favicons/android-icon-144x144.png and b/front/dist/static/images/favicons/android-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/android-icon-192x192.png b/front/dist/static/images/favicons/android-icon-192x192.png index 6f764aae..e1a4b3ed 100644 Binary files a/front/dist/static/images/favicons/android-icon-192x192.png and b/front/dist/static/images/favicons/android-icon-192x192.png differ diff --git a/front/dist/static/images/favicons/android-icon-36x36.png b/front/dist/static/images/favicons/android-icon-36x36.png index cc93c290..f9d822e9 100644 Binary files a/front/dist/static/images/favicons/android-icon-36x36.png and b/front/dist/static/images/favicons/android-icon-36x36.png differ diff --git a/front/dist/static/images/favicons/android-icon-48x48.png b/front/dist/static/images/favicons/android-icon-48x48.png index a2a7f7e9..35b1f177 100644 Binary files a/front/dist/static/images/favicons/android-icon-48x48.png and b/front/dist/static/images/favicons/android-icon-48x48.png differ diff --git a/front/dist/static/images/favicons/android-icon-72x72.png b/front/dist/static/images/favicons/android-icon-72x72.png index 9ae8c47a..b04f1b25 100644 Binary files a/front/dist/static/images/favicons/android-icon-72x72.png and b/front/dist/static/images/favicons/android-icon-72x72.png differ diff --git a/front/dist/static/images/favicons/android-icon-96x96.png b/front/dist/static/images/favicons/android-icon-96x96.png index 43324e2c..380de08a 100644 Binary files a/front/dist/static/images/favicons/android-icon-96x96.png and b/front/dist/static/images/favicons/android-icon-96x96.png differ diff --git a/front/dist/static/images/favicons/apple-icon-114x114.png b/front/dist/static/images/favicons/apple-icon-114x114.png index f205a3ad..6c994217 100644 Binary files a/front/dist/static/images/favicons/apple-icon-114x114.png and b/front/dist/static/images/favicons/apple-icon-114x114.png differ diff --git a/front/dist/static/images/favicons/apple-icon-120x120.png b/front/dist/static/images/favicons/apple-icon-120x120.png index 09f4e85d..b20bf1e2 100644 Binary files a/front/dist/static/images/favicons/apple-icon-120x120.png and b/front/dist/static/images/favicons/apple-icon-120x120.png differ diff --git a/front/dist/static/images/favicons/apple-icon-144x144.png b/front/dist/static/images/favicons/apple-icon-144x144.png index b0463804..59a7c4ce 100644 Binary files a/front/dist/static/images/favicons/apple-icon-144x144.png and b/front/dist/static/images/favicons/apple-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/apple-icon-152x152.png b/front/dist/static/images/favicons/apple-icon-152x152.png index fa06794f..fc905367 100644 Binary files a/front/dist/static/images/favicons/apple-icon-152x152.png and b/front/dist/static/images/favicons/apple-icon-152x152.png differ diff --git a/front/dist/static/images/favicons/apple-icon-180x180.png b/front/dist/static/images/favicons/apple-icon-180x180.png index 4b9af8b6..8f18bd51 100644 Binary files a/front/dist/static/images/favicons/apple-icon-180x180.png and b/front/dist/static/images/favicons/apple-icon-180x180.png differ diff --git a/front/dist/static/images/favicons/apple-icon-57x57.png b/front/dist/static/images/favicons/apple-icon-57x57.png index bd72841f..62c8ab07 100644 Binary files a/front/dist/static/images/favicons/apple-icon-57x57.png and b/front/dist/static/images/favicons/apple-icon-57x57.png differ diff --git a/front/dist/static/images/favicons/apple-icon-60x60.png b/front/dist/static/images/favicons/apple-icon-60x60.png index 89238b40..68c5b74b 100644 Binary files a/front/dist/static/images/favicons/apple-icon-60x60.png and b/front/dist/static/images/favicons/apple-icon-60x60.png differ diff --git a/front/dist/static/images/favicons/apple-icon-72x72.png b/front/dist/static/images/favicons/apple-icon-72x72.png index 9ae8c47a..b04f1b25 100644 Binary files a/front/dist/static/images/favicons/apple-icon-72x72.png and b/front/dist/static/images/favicons/apple-icon-72x72.png differ diff --git a/front/dist/static/images/favicons/apple-icon-76x76.png b/front/dist/static/images/favicons/apple-icon-76x76.png index fbd0c29c..a58cf3ed 100644 Binary files a/front/dist/static/images/favicons/apple-icon-76x76.png and b/front/dist/static/images/favicons/apple-icon-76x76.png differ diff --git a/front/dist/static/images/favicons/apple-icon-precomposed.png b/front/dist/static/images/favicons/apple-icon-precomposed.png index 3132ca5a..e1a4b3ed 100644 Binary files a/front/dist/static/images/favicons/apple-icon-precomposed.png and b/front/dist/static/images/favicons/apple-icon-precomposed.png differ diff --git a/front/dist/static/images/favicons/apple-icon.png b/front/dist/static/images/favicons/apple-icon.png index 3132ca5a..e1a4b3ed 100644 Binary files a/front/dist/static/images/favicons/apple-icon.png and b/front/dist/static/images/favicons/apple-icon.png differ diff --git a/front/dist/static/images/favicons/favicon-16x16.png b/front/dist/static/images/favicons/favicon-16x16.png index a49eb71a..cd608133 100644 Binary files a/front/dist/static/images/favicons/favicon-16x16.png and b/front/dist/static/images/favicons/favicon-16x16.png differ diff --git a/front/dist/static/images/favicons/favicon-32x32.png b/front/dist/static/images/favicons/favicon-32x32.png index 957f5006..a15ffef5 100644 Binary files a/front/dist/static/images/favicons/favicon-32x32.png and b/front/dist/static/images/favicons/favicon-32x32.png differ diff --git a/front/dist/static/images/favicons/favicon-96x96.png b/front/dist/static/images/favicons/favicon-96x96.png index 43324e2c..380de08a 100644 Binary files a/front/dist/static/images/favicons/favicon-96x96.png and b/front/dist/static/images/favicons/favicon-96x96.png differ diff --git a/front/dist/static/images/favicons/favicon.ico b/front/dist/static/images/favicons/favicon.ico index ec628ea2..203ce590 100644 Binary files a/front/dist/static/images/favicons/favicon.ico and b/front/dist/static/images/favicons/favicon.ico differ diff --git a/front/dist/static/images/favicons/icon-512x512.png b/front/dist/static/images/favicons/icon-512x512.png new file mode 100644 index 00000000..86cb7477 Binary files /dev/null and b/front/dist/static/images/favicons/icon-512x512.png differ diff --git a/front/dist/static/images/favicons/manifest.json b/front/dist/static/images/favicons/manifest.json index 47ad9377..30d08769 100644 --- a/front/dist/static/images/favicons/manifest.json +++ b/front/dist/static/images/favicons/manifest.json @@ -119,7 +119,13 @@ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192", "type": "image\/png", - "density": "4.0" + "density": "4.0", + "purpose": "any maskable" + }, + { + "src": "/static/images/favicons/icon-512x512.png", + "sizes": "512x512", + "type": "image\/png" } ], "start_url": "/", @@ -127,6 +133,7 @@ "display_override": ["window-control-overlay", "minimal-ui"], "display": "standalone", "scope": "/", + "lang": "en", "theme_color": "#000000", "shortcuts": [ { @@ -134,7 +141,7 @@ "short_name": "WA", "description": "WorkAdventure application", "url": "/", - "icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192" }] + "icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" }] } ], "description": "WorkAdventure application", diff --git a/front/dist/static/images/favicons/ms-icon-144x144.png b/front/dist/static/images/favicons/ms-icon-144x144.png index b0463804..59a7c4ce 100644 Binary files a/front/dist/static/images/favicons/ms-icon-144x144.png and b/front/dist/static/images/favicons/ms-icon-144x144.png differ diff --git a/front/dist/static/images/favicons/ms-icon-150x150.png b/front/dist/static/images/favicons/ms-icon-150x150.png index 4ab38ea3..3515b43a 100644 Binary files a/front/dist/static/images/favicons/ms-icon-150x150.png and b/front/dist/static/images/favicons/ms-icon-150x150.png differ diff --git a/front/dist/static/images/favicons/ms-icon-310x310.png b/front/dist/static/images/favicons/ms-icon-310x310.png index 56ceeb95..115ce84d 100644 Binary files a/front/dist/static/images/favicons/ms-icon-310x310.png and b/front/dist/static/images/favicons/ms-icon-310x310.png differ diff --git a/front/dist/static/images/favicons/ms-icon-70x70.png b/front/dist/static/images/favicons/ms-icon-70x70.png index 3fa12cae..342deff9 100644 Binary files a/front/dist/static/images/favicons/ms-icon-70x70.png and b/front/dist/static/images/favicons/ms-icon-70x70.png differ diff --git a/front/dist/static/images/send.png b/front/dist/static/images/send.png new file mode 100644 index 00000000..1f75634a Binary files /dev/null and b/front/dist/static/images/send.png differ diff --git a/front/package.json b/front/package.json index ee031668..4e4d66c9 100644 --- a/front/package.json +++ b/front/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@fontsource/press-start-2p": "^4.3.0", - "@types/simple-peer": "^9.6.0", + "@types/simple-peer": "^9.11.1", "@types/socket.io-client": "^1.4.32", "axios": "^0.21.1", "cross-env": "^7.0.3", @@ -51,7 +51,7 @@ "queue-typescript": "^1.0.1", "quill": "1.3.6", "rxjs": "^6.6.3", - "simple-peer": "^9.6.2", + "simple-peer": "^9.11.0", "socket.io-client": "^2.3.0", "standardized-audio-context": "^25.2.4" }, @@ -60,7 +60,8 @@ "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", - "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", + "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", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", diff --git a/front/src/Api/Events/DataLayerEvent.ts b/front/src/Api/Events/DataLayerEvent.ts index 096d6ef5..3062c1bc 100644 --- a/front/src/Api/Events/DataLayerEvent.ts +++ b/front/src/Api/Events/DataLayerEvent.ts @@ -1,13 +1,12 @@ import * as tg from "generic-type-guard"; - - -export const isDataLayerEvent = - new tg.IsInterface().withProperties({ - data: tg.isObject - }).get(); +export const isDataLayerEvent = new tg.IsInterface() + .withProperties({ + data: tg.isObject, + }) + .get(); /** * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers */ -export type DataLayerEvent = tg.GuardedType; \ No newline at end of file +export type DataLayerEvent = tg.GuardedType; diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 85fb37e9..edeeef80 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -1,14 +1,15 @@ import * as tg from "generic-type-guard"; -export const isGameStateEvent = - new tg.IsInterface().withProperties({ - roomId: tg.isString, - mapUrl: tg.isString, - nickname: tg.isUnion(tg.isString, tg.isNull), - uuid: tg.isUnion(tg.isString, tg.isUndefined), - startLayerName: tg.isUnion(tg.isString, tg.isNull), - tags : tg.isArray(tg.isString), - }).get(); +export const isGameStateEvent = new tg.IsInterface() + .withProperties({ + roomId: tg.isString, + mapUrl: tg.isString, + nickname: tg.isUnion(tg.isString, tg.isNull), + uuid: tg.isUnion(tg.isString, tg.isUndefined), + startLayerName: tg.isUnion(tg.isString, tg.isNull), + tags: tg.isArray(tg.isString), + }) + .get(); /** * A message sent from the game to the iFrame when the gameState is received by the script */ diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts index 50f017df..87b45482 100644 --- a/front/src/Api/Events/HasPlayerMovedEvent.ts +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -1,19 +1,17 @@ import * as tg from "generic-type-guard"; - - -export const isHasPlayerMovedEvent = - new tg.IsInterface().withProperties({ - direction: tg.isElementOf('right', 'left', 'up', 'down'), +export const isHasPlayerMovedEvent = new tg.IsInterface() + .withProperties({ + direction: tg.isElementOf("right", "left", "up", "down"), moving: tg.isBoolean, x: tg.isNumber, - y: tg.isNumber - }).get(); + y: tg.isNumber, + }) + .get(); /** * A message sent from the game to the iFrame to notify a movement from the current player. */ export type HasPlayerMovedEvent = tg.GuardedType; - -export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void +export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 7325f811..fc3384f8 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -1,73 +1,73 @@ - -import type { GameStateEvent } from './GameStateEvent'; -import type { ButtonClickedEvent } from './ButtonClickedEvent'; -import type { ChatEvent } from './ChatEvent'; -import type { ClosePopupEvent } from './ClosePopupEvent'; -import type { EnterLeaveEvent } from './EnterLeaveEvent'; -import type { GoToPageEvent } from './GoToPageEvent'; -import type { LoadPageEvent } from './LoadPageEvent'; -import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; -import type { OpenPopupEvent } from './OpenPopupEvent'; -import type { OpenTabEvent } from './OpenTabEvent'; -import type { UserInputChatEvent } from './UserInputChatEvent'; +import type { GameStateEvent } from "./GameStateEvent"; +import type { ButtonClickedEvent } from "./ButtonClickedEvent"; +import type { ChatEvent } from "./ChatEvent"; +import type { ClosePopupEvent } from "./ClosePopupEvent"; +import type { EnterLeaveEvent } from "./EnterLeaveEvent"; +import type { GoToPageEvent } from "./GoToPageEvent"; +import type { LoadPageEvent } from "./LoadPageEvent"; +import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent"; +import type { OpenPopupEvent } from "./OpenPopupEvent"; +import type { OpenTabEvent } from "./OpenTabEvent"; +import type { UserInputChatEvent } from "./UserInputChatEvent"; import type { DataLayerEvent } from "./DataLayerEvent"; -import type { LayerEvent } from './LayerEvent'; +import type { LayerEvent } from "./LayerEvent"; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; -import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; +import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; - +import type { SetTilesEvent } from "./SetTilesEvent"; export interface TypedMessageEvent extends MessageEvent { - data: T + data: T; } +/** + * List event types sent from an iFrame to WorkAdventure + */ export type IframeEventMap = { - //getState: GameStateEvent, - // updateTile: UpdateTileEvent - loadPage: LoadPageEvent - chat: ChatEvent, - openPopup: OpenPopupEvent - closePopup: ClosePopupEvent - openTab: OpenTabEvent - goToPage: GoToPageEvent - openCoWebSite: OpenCoWebSiteEvent - closeCoWebSite: null - disablePlayerControls: null - restorePlayerControls: null - displayBubble: null - removeBubble: null - onPlayerMove: undefined - showLayer: LayerEvent - hideLayer: LayerEvent - setProperty: SetPropertyEvent - getDataLayer: undefined - loadSound: LoadSoundEvent - playSound: PlaySoundEvent - stopSound: null, - getState: undefined, - registerMenuCommand: MenuItemRegisterEvent -} + loadPage: LoadPageEvent; + chat: ChatEvent; + openPopup: OpenPopupEvent; + closePopup: ClosePopupEvent; + openTab: OpenTabEvent; + goToPage: GoToPageEvent; + openCoWebSite: OpenCoWebSiteEvent; + closeCoWebSite: null; + disablePlayerControls: null; + restorePlayerControls: null; + displayBubble: null; + removeBubble: null; + onPlayerMove: undefined; + showLayer: LayerEvent; + hideLayer: LayerEvent; + setProperty: SetPropertyEvent; + getDataLayer: undefined; + loadSound: LoadSoundEvent; + playSound: PlaySoundEvent; + stopSound: null; + getState: undefined; + registerMenuCommand: MenuItemRegisterEvent; + setTiles: SetTilesEvent; +}; export interface IframeEvent { type: T; data: IframeEventMap[T]; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isIframeEventWrapper = (event: any): event is IframeEvent => typeof event.type === 'string'; +export const isIframeEventWrapper = (event: any): event is IframeEvent => + typeof event.type === "string"; export interface IframeResponseEventMap { - userInputChat: UserInputChatEvent - enterEvent: EnterLeaveEvent - leaveEvent: EnterLeaveEvent - buttonClickedEvent: ButtonClickedEvent - gameState: GameStateEvent - hasPlayerMoved: HasPlayerMovedEvent - dataLayer: DataLayerEvent - menuItemClicked: MenuItemClickedEvent + userInputChat: UserInputChatEvent; + enterEvent: EnterLeaveEvent; + leaveEvent: EnterLeaveEvent; + buttonClickedEvent: ButtonClickedEvent; + hasPlayerMoved: HasPlayerMovedEvent; + dataLayer: DataLayerEvent; + menuItemClicked: MenuItemClickedEvent; } export interface IframeResponseEvent { type: T; @@ -75,4 +75,49 @@ export interface IframeResponseEvent { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent => typeof event.type === 'string'; +export const isIframeResponseEventWrapper = (event: { + type?: string; +}): event is IframeResponseEvent => typeof event.type === "string"; + + +/** + * List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame + */ +export type IframeQueryMap = { + getState: { + query: undefined, + answer: GameStateEvent + }, +} + +export interface IframeQuery { + type: T; + data: IframeQueryMap[T]['query']; +} + +export interface IframeQueryWrapper { + id: number; + query: IframeQuery; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isIframeQuery = (event: any): event is IframeQuery => typeof event.type === 'string'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper => typeof event.id === 'number' && isIframeQuery(event.query); + +export interface IframeAnswerEvent { + id: number; + type: T; + data: IframeQueryMap[T]['answer']; +} + +export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number'; + +export interface IframeErrorAnswerEvent { + id: number; + type: keyof IframeQueryMap; + error: string; +} + +export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string'; diff --git a/front/src/Api/Events/LayerEvent.ts b/front/src/Api/Events/LayerEvent.ts index f854248b..b56c3163 100644 --- a/front/src/Api/Events/LayerEvent.ts +++ b/front/src/Api/Events/LayerEvent.ts @@ -1,9 +1,10 @@ import * as tg from "generic-type-guard"; -export const isLayerEvent = - new tg.IsInterface().withProperties({ +export const isLayerEvent = new tg.IsInterface() + .withProperties({ name: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to show/hide a layer. */ diff --git a/front/src/Api/Events/LoadPageEvent.ts b/front/src/Api/Events/LoadPageEvent.ts index 9bc7f32a..63600a28 100644 --- a/front/src/Api/Events/LoadPageEvent.ts +++ b/front/src/Api/Events/LoadPageEvent.ts @@ -1,13 +1,12 @@ import * as tg from "generic-type-guard"; - - -export const isLoadPageEvent = - new tg.IsInterface().withProperties({ +export const isLoadPageEvent = new tg.IsInterface() + .withProperties({ url: tg.isString, - }).get(); + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. */ -export type LoadPageEvent = tg.GuardedType; \ No newline at end of file +export type LoadPageEvent = tg.GuardedType; diff --git a/front/src/Api/Events/OpenCoWebSiteEvent.ts b/front/src/Api/Events/OpenCoWebSiteEvent.ts index 0fbc0ce2..7b5e6070 100644 --- a/front/src/Api/Events/OpenCoWebSiteEvent.ts +++ b/front/src/Api/Events/OpenCoWebSiteEvent.ts @@ -1,11 +1,12 @@ import * as tg from "generic-type-guard"; - - -export const isOpenCoWebsite = - new tg.IsInterface().withProperties({ +export const isOpenCoWebsite = new tg.IsInterface() + .withProperties({ url: tg.isString, - }).get(); + allowApi: tg.isBoolean, + allowPolicy: tg.isString, + }) + .get(); /** * A message sent from the iFrame to the game to add a message in the chat. diff --git a/front/src/Api/Events/SetTilesEvent.ts b/front/src/Api/Events/SetTilesEvent.ts new file mode 100644 index 00000000..371f0884 --- /dev/null +++ b/front/src/Api/Events/SetTilesEvent.ts @@ -0,0 +1,16 @@ +import * as tg from "generic-type-guard"; + +export const isSetTilesEvent = tg.isArray( + new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull), + layer: tg.isString, + }) + .get() +); +/** + * A message sent from the iFrame to the game to set one or many tiles. + */ +export type SetTilesEvent = tg.GuardedType; diff --git a/front/src/Api/Events/setPropertyEvent.ts b/front/src/Api/Events/setPropertyEvent.ts index 39785bc6..7335f781 100644 --- a/front/src/Api/Events/setPropertyEvent.ts +++ b/front/src/Api/Events/setPropertyEvent.ts @@ -1,12 +1,13 @@ import * as tg from "generic-type-guard"; -export const isSetPropertyEvent = - new tg.IsInterface().withProperties({ +export const isSetPropertyEvent = new tg.IsInterface() + .withProperties({ layerName: tg.isString, propertyName: tg.isString, - propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))) - }).get(); + propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))), + }) + .get(); /** * A message sent from the iFrame to the game to change the value of the property of the layer */ -export type SetPropertyEvent = tg.GuardedType; \ No newline at end of file +export type SetPropertyEvent = tg.GuardedType; diff --git a/front/src/Api/Events/ui/MenuItemClickedEvent.ts b/front/src/Api/Events/ui/MenuItemClickedEvent.ts index fad2944f..a8c8d0ed 100644 --- a/front/src/Api/Events/ui/MenuItemClickedEvent.ts +++ b/front/src/Api/Events/ui/MenuItemClickedEvent.ts @@ -1,12 +1,11 @@ import * as tg from "generic-type-guard"; -export const isMenuItemClickedEvent = - new tg.IsInterface().withProperties({ - menuItem: tg.isString - }).get(); +export const isMenuItemClickedEvent = new tg.IsInterface() + .withProperties({ + menuItem: tg.isString, + }) + .get(); /** * A message sent from the game to the iFrame when a menu item is clicked. */ export type MenuItemClickedEvent = tg.GuardedType; - - diff --git a/front/src/Api/Events/ui/MenuItemRegisterEvent.ts b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts index 4a56d8a0..404bdb13 100644 --- a/front/src/Api/Events/ui/MenuItemRegisterEvent.ts +++ b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts @@ -1,25 +1,26 @@ import * as tg from "generic-type-guard"; -import { Subject } from 'rxjs'; +import { Subject } from "rxjs"; -export const isMenuItemRegisterEvent = - new tg.IsInterface().withProperties({ - menutItem: tg.isString - }).get(); +export const isMenuItemRegisterEvent = new tg.IsInterface() + .withProperties({ + menutItem: tg.isString, + }) + .get(); /** * A message sent from the iFrame to the game to add a new menu item. */ export type MenuItemRegisterEvent = tg.GuardedType; -export const isMenuItemRegisterIframeEvent = - new tg.IsInterface().withProperties({ +export const isMenuItemRegisterIframeEvent = new tg.IsInterface() + .withProperties({ type: tg.isSingletonString("registerMenuCommand"), - data: isMenuItemRegisterEvent - }).get(); - + data: isMenuItemRegisterEvent, + }) + .get(); const _registerMenuCommandStream: Subject = new Subject(); export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { - _registerMenuCommandStream.next(event.menutItem) -} \ No newline at end of file + _registerMenuCommandStream.next(event.menutItem); +} diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 9311d7b6..314d5d2e 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,42 +1,45 @@ -import {Subject} from "rxjs"; -import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; -import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; -import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; -import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; -import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; -import {scriptUtils} from "./ScriptUtils"; -import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; -import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { Subject } from "rxjs"; +import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; +import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; +import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; +import { scriptUtils } from "./ScriptUtils"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { + IframeErrorAnswerEvent, IframeEvent, - IframeEventMap, + IframeEventMap, IframeQueryMap, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, - TypedMessageEvent + isIframeQueryWrapper, + TypedMessageEvent, } from "./Events/IframeEvent"; -import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; -//import { isLoadPageEvent } from './Events/LoadPageEvent'; -import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; -import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; -import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; -import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; -import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; -import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent"; -import type {DataLayerEvent} from "./Events/DataLayerEvent"; -import type {GameStateEvent} from "./Events/GameStateEvent"; -import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; -import {isLoadPageEvent} from "./Events/LoadPageEvent"; -import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent"; +import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; +import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; +import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; +import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; +import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"; +import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; +import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent"; +import type { DataLayerEvent } from "./Events/DataLayerEvent"; +import type { GameStateEvent } from "./Events/GameStateEvent"; +import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; +import { isLoadPageEvent } from "./Events/LoadPageEvent"; +import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; +import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; + +type AnswererCallback = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise; /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. */ class IframeListener { - private readonly _chatStream: Subject = new Subject(); public readonly chatStream = this._chatStream.asObservable(); @@ -82,9 +85,6 @@ class IframeListener { private readonly _setPropertyStream: Subject = new Subject(); public readonly setPropertyStream = this._setPropertyStream.asObservable(); - private readonly _gameStateStream: Subject = new Subject(); - public readonly gameStateStream = this._gameStateStream.asObservable(); - private readonly _dataLayerChangeStream: Subject = new Subject(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); @@ -103,111 +103,155 @@ class IframeListener { private readonly _loadSoundStream: Subject = new Subject(); public readonly loadSoundStream = this._loadSoundStream.asObservable(); + private readonly _setTilesStream: Subject = new Subject(); + public readonly setTilesStream = this._setTilesStream.asObservable(); + private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; + private answerers: { + [key in keyof IframeQueryMap]?: AnswererCallback + } = {}; + init() { - window.addEventListener("message", (message: TypedMessageEvent>) => { - // Do we trust the sender of this message? - // Let's only accept messages from the iframe that are allowed. - // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). - let foundSrc: string | undefined; + window.addEventListener( + "message", + (message: TypedMessageEvent>) => { + // Do we trust the sender of this message? + // Let's only accept messages from the iframe that are allowed. + // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). + let foundSrc: string | undefined; - let iframe: HTMLIFrameElement; - for (iframe of this.iframes) { - if (iframe.contentWindow === message.source) { - foundSrc = iframe.src; - break; - } - } - - if (foundSrc === undefined) { - return; - } - - const payload = message.data; - if (isIframeEventWrapper(payload)) { - if (payload.type === 'showLayer' && isLayerEvent(payload.data)) { - this._showLayerStream.next(payload.data); - } else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) { - this._hideLayerStream.next(payload.data); - } else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) { - this._setPropertyStream.next(payload.data); - } else if (payload.type === 'chat' && isChatEvent(payload.data)) { - this._chatStream.next(payload.data); - } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { - this._openPopupStream.next(payload.data); - } else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) { - this._closePopupStream.next(payload.data); - } - else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) { - scriptUtils.openTab(payload.data.url); - } - else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { - scriptUtils.goToPage(payload.data.url); - } - else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { - this._loadPageStream.next(payload.data.url); - } - else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) { - this._playSoundStream.next(payload.data); - } - else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) { - this._stopSoundStream.next(payload.data); - } - else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) { - this._loadSoundStream.next(payload.data); - } - else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { - scriptUtils.openCoWebsite(payload.data.url, foundSrc); + let iframe: HTMLIFrameElement | undefined; + for (iframe of this.iframes) { + if (iframe.contentWindow === message.source) { + foundSrc = iframe.src; + break; + } } - else if (payload.type === 'closeCoWebSite') { - scriptUtils.closeCoWebSite(); + const payload = message.data; + + if (foundSrc === undefined || iframe === undefined) { + if (isIframeEventWrapper(payload)) { + console.warn( + "It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " + + "If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " + + 'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' + + "parameter to WA.nav.openCoWebSite())" + ); + } + return; } - else if (payload.type === 'disablePlayerControls') { - this._disablePlayerControlStream.next(); - } - else if (payload.type === 'restorePlayerControls') { - this._enablePlayerControlStream.next(); - } else if (payload.type === 'displayBubble') { - this._displayBubbleStream.next(); - } else if (payload.type === 'removeBubble') { - this._removeBubbleStream.next(); - } else if (payload.type == "getState") { - this._gameStateStream.next(); - } else if (payload.type == "onPlayerMove") { - this.sendPlayerMove = true - } else if (payload.type == "getDataLayer") { - this._dataLayerChangeStream.next(); - } else if (isMenuItemRegisterIframeEvent(payload)) { - const data = payload.data.menutItem; - // @ts-ignore - this.iframeCloseCallbacks.get(iframe).push(() => { - this._unregisterMenuCommandStream.next(data); - }) - handleMenuItemRegistrationEvent(payload.data) - } - } - }, false); + foundSrc = this.getBaseUrl(foundSrc, message.source); + if (isIframeQueryWrapper(payload)) { + const queryId = payload.id; + const query = payload.query; + + const answerer = this.answerers[query.type]; + if (answerer === undefined) { + const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.'; + console.error(errorMsg); + iframe.contentWindow?.postMessage({ + id: queryId, + type: query.type, + error: errorMsg + } as IframeErrorAnswerEvent, '*'); + return; + } + + Promise.resolve(answerer(query.data)).then((value) => { + iframe?.contentWindow?.postMessage({ + id: queryId, + type: query.type, + data: value + }, '*'); + }).catch(reason => { + console.error('An error occurred while responding to an iFrame query.', reason); + let reasonMsg: string; + if (reason instanceof Error) { + reasonMsg = reason.message; + } else { + reasonMsg = reason.toString(); + } + + iframe?.contentWindow?.postMessage({ + id: queryId, + type: query.type, + error: reasonMsg + } as IframeErrorAnswerEvent, '*'); + }); + + } else if (isIframeEventWrapper(payload)) { + if (payload.type === "showLayer" && isLayerEvent(payload.data)) { + this._showLayerStream.next(payload.data); + } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) { + this._hideLayerStream.next(payload.data); + } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { + this._setPropertyStream.next(payload.data); + } else if (payload.type === "chat" && isChatEvent(payload.data)) { + this._chatStream.next(payload.data); + } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { + this._openPopupStream.next(payload.data); + } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { + this._closePopupStream.next(payload.data); + } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) { + scriptUtils.openTab(payload.data.url); + } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) { + scriptUtils.goToPage(payload.data.url); + } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) { + this._loadPageStream.next(payload.data.url); + } else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) { + this._playSoundStream.next(payload.data); + } else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) { + this._stopSoundStream.next(payload.data); + } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { + this._loadSoundStream.next(payload.data); + } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) { + scriptUtils.openCoWebsite( + payload.data.url, + foundSrc, + payload.data.allowApi, + payload.data.allowPolicy + ); + } else if (payload.type === "closeCoWebSite") { + scriptUtils.closeCoWebSite(); + } else if (payload.type === "disablePlayerControls") { + this._disablePlayerControlStream.next(); + } else if (payload.type === "restorePlayerControls") { + this._enablePlayerControlStream.next(); + } else if (payload.type === "displayBubble") { + this._displayBubbleStream.next(); + } else if (payload.type === "removeBubble") { + this._removeBubbleStream.next(); + } else if (payload.type == "onPlayerMove") { + this.sendPlayerMove = true; + } else if (payload.type == "getDataLayer") { + this._dataLayerChangeStream.next(); + } else if (isMenuItemRegisterIframeEvent(payload)) { + const data = payload.data.menutItem; + // @ts-ignore + this.iframeCloseCallbacks.get(iframe).push(() => { + this._unregisterMenuCommandStream.next(data); + }); + handleMenuItemRegistrationEvent(payload.data); + } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { + this._setTilesStream.next(payload.data); + } + } + }, + false + ); } sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ - 'type' : 'dataLayer', - 'data' : dataLayerEvent - }) - } - - - sendGameStateEvent(gameStateEvent: GameStateEvent) { - this.postMessage({ - 'type': 'gameState', - 'data': gameStateEvent + type: "dataLayer", + data: dataLayerEvent, }); } @@ -220,25 +264,25 @@ class IframeListener { } unregisterIframe(iframe: HTMLIFrameElement): void { - this.iframeCloseCallbacks.get(iframe)?.forEach(callback => { + this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => { callback(); }); this.iframes.delete(iframe); } registerScript(scriptUrl: string): void { - console.log('Loading map related script at ', scriptUrl) + console.log("Loading map related script at ", scriptUrl); - if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { + if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { // Using external iframe mode ( - const iframe = document.createElement('iframe'); + const iframe = document.createElement("iframe"); iframe.id = IframeListener.getIFrameId(scriptUrl); - iframe.style.display = 'none'; - iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); + iframe.style.display = "none"; + iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl); // We are putting a sandbox on this script because it will run in the same domain as the main website. - iframe.sandbox.add('allow-scripts'); - iframe.sandbox.add('allow-top-navigation-by-user-activation'); + iframe.sandbox.add("allow-scripts"); + iframe.sandbox.add("allow-top-navigation-by-user-activation"); document.body.prepend(iframe); @@ -246,36 +290,50 @@ class IframeListener { this.registerIframe(iframe); } else { // production code - const iframe = document.createElement('iframe'); + const iframe = document.createElement("iframe"); iframe.id = IframeListener.getIFrameId(scriptUrl); - iframe.style.display = 'none'; + iframe.style.display = "none"; // We are putting a sandbox on this script because it will run in the same domain as the main website. - iframe.sandbox.add('allow-scripts'); - iframe.sandbox.add('allow-top-navigation-by-user-activation'); + iframe.sandbox.add("allow-scripts"); + iframe.sandbox.add("allow-top-navigation-by-user-activation"); //iframe.src = "data:text/html;charset=utf-8," + escape(html); - iframe.srcdoc = '\n' + - '\n' + + iframe.srcdoc = + "\n" + + "\n" + '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n' + - '\n'; + "\n" + + '\n' + + '\n' + + "\n" + + "\n" + + "\n"; document.body.prepend(iframe); this.scripts.set(scriptUrl, iframe); this.registerIframe(iframe); } + } - + private getBaseUrl(src: string, source: MessageEventSource | null): string { + for (const script of this.scripts) { + if (script[1].contentWindow === source) { + return script[0]; + } + } + return src; } private static getIFrameId(scriptUrl: string): string { - return 'script' + btoa(scriptUrl); + return "script" + btoa(scriptUrl); } unregisterScript(scriptUrl: string): void { @@ -292,47 +350,47 @@ class IframeListener { sendUserInputChat(message: string) { this.postMessage({ - 'type': 'userInputChat', - 'data': { - 'message': message, - } as UserInputChatEvent + type: "userInputChat", + data: { + message: message, + } as UserInputChatEvent, }); } sendEnterEvent(name: string) { this.postMessage({ - 'type': 'enterEvent', - 'data': { - "name": name - } as EnterLeaveEvent + type: "enterEvent", + data: { + name: name, + } as EnterLeaveEvent, }); } sendLeaveEvent(name: string) { this.postMessage({ - 'type': 'leaveEvent', - 'data': { - "name": name - } as EnterLeaveEvent + type: "leaveEvent", + data: { + name: name, + } as EnterLeaveEvent, }); } hasPlayerMoved(event: HasPlayerMovedEvent) { if (this.sendPlayerMove) { this.postMessage({ - 'type': 'hasPlayerMoved', - 'data': event + type: "hasPlayerMoved", + data: event, }); } } sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ - 'type': 'buttonClickedEvent', - 'data': { + type: "buttonClickedEvent", + data: { popupId, - buttonId - } as ButtonClickedEvent + buttonId, + } as ButtonClickedEvent, }); } @@ -341,10 +399,25 @@ class IframeListener { */ public postMessage(message: IframeResponseEvent) { for (const iframe of this.iframes) { - iframe.contentWindow?.postMessage(message, '*'); + iframe.contentWindow?.postMessage(message, "*"); } } + /** + * Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type). + * + * Important! There can be only one "answerer" so registering a new one will unregister the old one. + * + * @param key The "type" of the query we are answering + * @param callback + */ + public registerAnswerer(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise ): void { + this.answerers[key] = callback; + } + + public unregisterAnswerer(key: keyof IframeQueryMap): void { + delete this.answerers[key]; + } } export const iframeListener = new IframeListener(); diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts index e1c94507..0dbe40fe 100644 --- a/front/src/Api/ScriptUtils.ts +++ b/front/src/Api/ScriptUtils.ts @@ -1,21 +1,19 @@ -import {coWebsiteManager} from "../WebRtc/CoWebsiteManager"; +import { coWebsiteManager } from "../WebRtc/CoWebsiteManager"; class ScriptUtils { - - public openTab(url : string){ + public openTab(url: string) { window.open(url); } - public goToPage(url : string){ - window.location.href = url; - + public goToPage(url: string) { + window.location.href = url; } - public openCoWebsite(url: string, base: string) { - coWebsiteManager.loadCoWebsite(url, base); + public openCoWebsite(url: string, base: string, api: boolean, policy: string) { + coWebsiteManager.loadCoWebsite(url, base, api, policy); } - public closeCoWebSite(){ + public closeCoWebSite() { coWebsiteManager.closeCoWebsite(); } } diff --git a/front/src/Api/iframe/IframeApiContribution.ts b/front/src/Api/iframe/IframeApiContribution.ts index f3b25999..e4ba089e 100644 --- a/front/src/Api/iframe/IframeApiContribution.ts +++ b/front/src/Api/iframe/IframeApiContribution.ts @@ -1,9 +1,40 @@ import type * as tg from "generic-type-guard"; -import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; +import type { + IframeEvent, + IframeEventMap, IframeQuery, + IframeQueryMap, + IframeResponseEventMap +} from '../Events/IframeEvent'; +import type {IframeQueryWrapper} from "../Events/IframeEvent"; export function sendToWorkadventure(content: IframeEvent) { window.parent.postMessage(content, "*") } + +let queryNumber = 0; + +export const answerPromises = new Map)) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject: (reason?: any) => void +}>(); + +export function queryWorkadventure(content: IframeQuery): Promise { + return new Promise((resolve, reject) => { + window.parent.postMessage({ + id: queryNumber, + query: content + } as IframeQueryWrapper, "*"); + + answerPromises.set(queryNumber, { + resolve, + reject + }); + + queryNumber++; + }); +} + type GuardedType> = Guard extends tg.TypeGuard ? T : never export interface IframeCallback> { diff --git a/front/src/Api/iframe/Ui/MenuItem.ts b/front/src/Api/iframe/Ui/MenuItem.ts index 9782ea7a..aa61d749 100644 --- a/front/src/Api/iframe/Ui/MenuItem.ts +++ b/front/src/Api/iframe/Ui/MenuItem.ts @@ -1,11 +1,11 @@ -import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent'; -import { iframeListener } from '../../IframeListener'; +import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent"; +import { iframeListener } from "../../IframeListener"; export function sendMenuClickedEvent(menuItem: string) { iframeListener.postMessage({ - 'type': 'menuItemClicked', - 'data': { + type: "menuItemClicked", + data: { menuItem: menuItem, - } as MenuItemClickedEvent + } as MenuItemClickedEvent, }); -} \ No newline at end of file +} diff --git a/front/src/Api/iframe/chat.ts b/front/src/Api/iframe/chat.ts index 7d8e6f71..5797df5a 100644 --- a/front/src/Api/iframe/chat.ts +++ b/front/src/Api/iframe/chat.ts @@ -1,30 +1,30 @@ -import type { ChatEvent } from '../Events/ChatEvent' -import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent' -import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution' +import type { ChatEvent } from "../Events/ChatEvent"; +import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent"; +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; -import {Subject} from "rxjs"; +import { Subject } from "rxjs"; const chatStream = new Subject(); -class WorkadventureChatCommands extends IframeApiContribution { - - callbacks = [apiCallback({ - callback: (event: UserInputChatEvent) => { - chatStream.next(event.message); - }, - type: "userInputChat", - typeChecker: isUserInputChatEvent - })] - +export class WorkadventureChatCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + callback: (event: UserInputChatEvent) => { + chatStream.next(event.message); + }, + type: "userInputChat", + typeChecker: isUserInputChatEvent, + }), + ]; sendChatMessage(message: string, author: string) { sendToWorkadventure({ - type: 'chat', + type: "chat", data: { - 'message': message, - 'author': author - } - }) + message: message, + author: author, + }, + }); } /** @@ -35,4 +35,4 @@ class WorkadventureChatCommands extends IframeApiContribution { - callbacks = [] +export class WorkadventureControlsCommands extends IframeApiContribution { + callbacks = []; disablePlayerControls(): void { - sendToWorkadventure({ 'type': 'disablePlayerControls', data: null }); + sendToWorkadventure({ type: "disablePlayerControls", data: null }); } restorePlayerControls(): void { - sendToWorkadventure({ 'type': 'restorePlayerControls', data: null }); + sendToWorkadventure({ type: "restorePlayerControls", data: null }); } } - export default new WorkadventureControlsCommands(); diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index 0d31c1ea..f051a7c0 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -1,57 +1,56 @@ -import type { GoToPageEvent } from '../Events/GoToPageEvent'; -import type { OpenTabEvent } from '../Events/OpenTabEvent'; -import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; -import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent"; -import type {LoadPageEvent} from "../Events/LoadPageEvent"; - - -class WorkadventureNavigationCommands extends IframeApiContribution { - callbacks = [] +import type { GoToPageEvent } from "../Events/GoToPageEvent"; +import type { OpenTabEvent } from "../Events/OpenTabEvent"; +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; +import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent"; +import type { LoadPageEvent } from "../Events/LoadPageEvent"; +export class WorkadventureNavigationCommands extends IframeApiContribution { + callbacks = []; openTab(url: string): void { sendToWorkadventure({ - "type": 'openTab', - "data": { - url - } + type: "openTab", + data: { + url, + }, }); } goToPage(url: string): void { sendToWorkadventure({ - "type": 'goToPage', - "data": { - url - } + type: "goToPage", + data: { + url, + }, }); } goToRoom(url: string): void { sendToWorkadventure({ - "type": 'loadPage', - "data": { - url - } + type: "loadPage", + data: { + url, + }, }); } - openCoWebSite(url: string): void { + openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void { sendToWorkadventure({ - "type": 'openCoWebSite', - "data": { - url - } + type: "openCoWebSite", + data: { + url, + allowApi, + allowPolicy, + }, }); } closeCoWebSite(): void { sendToWorkadventure({ - "type": 'closeCoWebSite', - data: null + type: "closeCoWebSite", + data: null, }); } } - export default new WorkadventureNavigationCommands(); diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index 67c012f7..cad66a36 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -1,29 +1,41 @@ -import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution"; -import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent"; -import {Subject} from "rxjs"; -import {apiCallback} from "./registeredCallbacks"; -import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent"; +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; +import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent"; +import { Subject } from "rxjs"; +import { apiCallback } from "./registeredCallbacks"; +import { getGameState } from "./room"; +import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; + +interface User { + id: string | undefined; + nickName: string | null; + tags: string[]; +} const moveStream = new Subject(); -class WorkadventurePlayerCommands extends IframeApiContribution { +export class WorkadventurePlayerCommands extends IframeApiContribution { callbacks = [ apiCallback({ - type: 'hasPlayerMoved', + type: "hasPlayerMoved", typeChecker: isHasPlayerMovedEvent, callback: (payloadData) => { moveStream.next(payloadData); - } + }, }), - ] + ]; onPlayerMove(callback: HasPlayerMovedEventCallback): void { moveStream.subscribe(callback); sendToWorkadventure({ - type: 'onPlayerMove', - data: null - }) + type: "onPlayerMove", + data: null, + }); + } + getCurrentUser(): Promise { + return getGameState().then((gameState) => { + return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags }; + }); } } -export default new WorkadventurePlayerCommands(); \ No newline at end of file +export default new WorkadventurePlayerCommands(); diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index aed4d983..0256dfc8 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -1,87 +1,74 @@ import { Subject } from "rxjs"; -import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent'; -import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution'; + +import { isDataLayerEvent } from "../Events/DataLayerEvent"; +import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; +import { isGameStateEvent } from "../Events/GameStateEvent"; + +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; -import type {LayerEvent} from "../Events/LayerEvent"; -import type {SetPropertyEvent} from "../Events/setPropertyEvent"; -import type {GameStateEvent} from "../Events/GameStateEvent"; -import type {ITiledMap} from "../../Phaser/Map/ITiledMap"; -import type {DataLayerEvent} from "../Events/DataLayerEvent"; -import {isGameStateEvent} from "../Events/GameStateEvent"; -import {isDataLayerEvent} from "../Events/DataLayerEvent"; + +import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; +import type { DataLayerEvent } from "../Events/DataLayerEvent"; +import type { GameStateEvent } from "../Events/GameStateEvent"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); const dataLayerResolver = new Subject(); -const stateResolvers = new Subject(); -let immutableData: GameStateEvent; +let immutableDataPromise: Promise | undefined = undefined; interface Room { - id: string, - mapUrl: string, - map: ITiledMap, - startLayer: string | null + id: string; + mapUrl: string; + map: ITiledMap; + startLayer: string | null; } -interface User { - id: string | undefined, - nickName: string | null, - tags: string[] +interface TileDescriptor { + x: number; + y: number; + tile: number | string | null; + layer: string; } - -function getGameState(): Promise { - if (immutableData) { - return Promise.resolve(immutableData); - } - else { - return new Promise((resolver, thrower) => { - stateResolvers.subscribe(resolver); - sendToWorkadventure({type: "getState", data: null}); - }) +export function getGameState(): Promise { + if (immutableDataPromise === undefined) { + immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined }); } + return immutableDataPromise; } function getDataLayer(): Promise { return new Promise((resolver, thrower) => { dataLayerResolver.subscribe(resolver); - sendToWorkadventure({type: "getDataLayer", data: null}) - }) + sendToWorkadventure({ type: "getDataLayer", data: null }); + }); } -class WorkadventureRoomCommands extends IframeApiContribution { +export class WorkadventureRoomCommands extends IframeApiContribution { callbacks = [ apiCallback({ callback: (payloadData: EnterLeaveEvent) => { enterStreams.get(payloadData.name)?.next(); }, type: "enterEvent", - typeChecker: isEnterLeaveEvent + typeChecker: isEnterLeaveEvent, }), apiCallback({ type: "leaveEvent", typeChecker: isEnterLeaveEvent, callback: (payloadData) => { leaveStreams.get(payloadData.name)?.next(); - } - }), - apiCallback({ - type: "gameState", - typeChecker: isGameStateEvent, - callback: (payloadData) => { - stateResolvers.next(payloadData); - } + }, }), apiCallback({ type: "dataLayer", typeChecker: isDataLayerEvent, callback: (payloadData) => { dataLayerResolver.next(payloadData); - } + }, }), - ] - + ]; onEnterZone(name: string, callback: () => void): void { let subject = enterStreams.get(name); @@ -90,7 +77,6 @@ class WorkadventureRoomCommands extends IframeApiContribution void): void { let subject = leaveStreams.get(name); @@ -101,35 +87,39 @@ class WorkadventureRoomCommands extends IframeApiContribution { return getGameState().then((gameState) => { - return getDataLayer().then((mapJson) => { - return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName}; - }) - }) + return getDataLayer().then((mapJson) => { + return { + id: gameState.roomId, + map: mapJson.data as ITiledMap, + mapUrl: gameState.mapUrl, + startLayer: gameState.startLayerName, + }; + }); + }); } - getCurrentUser(): Promise { - return getGameState().then((gameState) => { - return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; - }) + setTiles(tiles: TileDescriptor[]) { + sendToWorkadventure({ + type: "setTiles", + data: tiles, + }); } - } - export default new WorkadventureRoomCommands(); diff --git a/front/src/Api/iframe/sound.ts b/front/src/Api/iframe/sound.ts index 70430b46..1e5d6157 100644 --- a/front/src/Api/iframe/sound.ts +++ b/front/src/Api/iframe/sound.ts @@ -1,17 +1,15 @@ -import type { LoadSoundEvent } from '../Events/LoadSoundEvent'; -import type { PlaySoundEvent } from '../Events/PlaySoundEvent'; -import type { StopSoundEvent } from '../Events/StopSoundEvent'; -import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; -import {Sound} from "./Sound/Sound"; +import type { LoadSoundEvent } from "../Events/LoadSoundEvent"; +import type { PlaySoundEvent } from "../Events/PlaySoundEvent"; +import type { StopSoundEvent } from "../Events/StopSoundEvent"; +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; +import { Sound } from "./Sound/Sound"; -class WorkadventureSoundCommands extends IframeApiContribution { - callbacks = [] +export class WorkadventureSoundCommands extends IframeApiContribution { + callbacks = []; loadSound(url: string): Sound { return new Sound(url); } - } - export default new WorkadventureSoundCommands(); diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index c7655b84..61c7076e 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -1,53 +1,55 @@ -import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; -import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; -import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent'; -import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; +import { isButtonClickedEvent } from "../Events/ButtonClickedEvent"; +import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent"; +import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent"; +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; import { Popup } from "./Ui/Popup"; let popupId = 0; const popups: Map = new Map(); -const popupCallbacks: Map> = new Map>(); +const popupCallbacks: Map> = new Map< + number, + Map +>(); -const menuCallbacks: Map void> = new Map() +const menuCallbacks: Map void> = new Map(); interface ZonedPopupOptions { - zone: string - objectLayerName?: string, - popupText: string, - delay?: number - popupOptions: Array + zone: string; + objectLayerName?: string; + popupText: string; + delay?: number; + popupOptions: Array; } - -class WorkAdventureUiCommands extends IframeApiContribution { - - callbacks = [apiCallback({ - type: "buttonClickedEvent", - typeChecker: isButtonClickedEvent, - callback: (payloadData) => { - const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); - const popup = popups.get(payloadData.popupId); - if (popup === undefined) { - throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); - } - if (callback) { - callback(popup); - } - } - }), - apiCallback({ - type: "menuItemClicked", - typeChecker: isMenuItemClickedEvent, - callback: event => { - const callback = menuCallbacks.get(event.menuItem); - if (callback) { - callback(event.menuItem) - } - } - })]; - +export class WorkAdventureUiCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + type: "buttonClickedEvent", + typeChecker: isButtonClickedEvent, + callback: (payloadData) => { + const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); + const popup = popups.get(payloadData.popupId); + if (popup === undefined) { + throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); + } + if (callback) { + callback(popup); + } + }, + }), + apiCallback({ + type: "menuItemClicked", + typeChecker: isMenuItemClickedEvent, + callback: (event) => { + const callback = menuCallbacks.get(event.menuItem); + if (callback) { + callback(event.menuItem); + } + }, + }), + ]; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { popupId++; @@ -66,40 +68,40 @@ class WorkAdventureUiCommands extends IframeApiContribution { return { label: button.label, - className: button.className + className: button.className, }; - }) - } + }), + }, }); - popups.set(popupId, popup) + popups.set(popupId, popup); return popup; } registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { menuCallbacks.set(commandDescriptor, callback); sendToWorkadventure({ - 'type': 'registerMenuCommand', - 'data': { - menutItem: commandDescriptor - } + type: "registerMenuCommand", + data: { + menutItem: commandDescriptor, + }, }); } displayBubble(): void { - sendToWorkadventure({ 'type': 'displayBubble', data: null }); + sendToWorkadventure({ type: "displayBubble", data: null }); } removeBubble(): void { - sendToWorkadventure({ 'type': 'removeBubble', data: null }); + sendToWorkadventure({ type: "removeBubble", data: null }); } } diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index 8ade9398..0f808074 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -10,12 +10,14 @@ import {errorStore} from "../Stores/ErrorStore"; import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte"; import LoginScene from "./Login/LoginScene.svelte"; + import Chat from "./Chat/Chat.svelte"; import {loginSceneVisibleStore} from "../Stores/LoginSceneStore"; import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte"; import VisitCard from "./VisitCard/VisitCard.svelte"; import {requestVisitCardsStore} from "../Stores/GameStore"; import type {Game} from "../Phaser/Game/Game"; + import {chatVisibilityStore} from "../Stores/ChatStore"; import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"; import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte"; @@ -61,14 +63,6 @@
{/if} - - {#if $gameOverlayVisibilityStore}
@@ -94,4 +88,7 @@
{/if} + {#if $chatVisibilityStore} + + {/if}
diff --git a/front/src/Components/Chat/Chat.svelte b/front/src/Components/Chat/Chat.svelte new file mode 100644 index 00000000..e39d1a59 --- /dev/null +++ b/front/src/Components/Chat/Chat.svelte @@ -0,0 +1,101 @@ + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatElement.svelte b/front/src/Components/Chat/ChatElement.svelte new file mode 100644 index 00000000..66ed724b --- /dev/null +++ b/front/src/Components/Chat/ChatElement.svelte @@ -0,0 +1,83 @@ + + +
+
+ {#if message.type === ChatMessageTypes.userIncoming} + >> {#each targets as target, index}{#if !isLastIteration(index)}, {/if}{/each} entered ({renderDate(message.date)}) + {:else if message.type === ChatMessageTypes.userOutcoming} + << {#each targets as target, index}{#if !isLastIteration(index)}, {/if}{/each} left ({renderDate(message.date)}) + {:else if message.type === ChatMessageTypes.me} +

Me: ({renderDate(message.date)})

+ {#each texts as text} +

{@html urlifyText(text)}

+ {/each} + {:else} +

: ({renderDate(message.date)})

+ {#each texts as text} +

{@html urlifyText(text)}

+ {/each} + {/if} +
+
+ + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatMessageForm.svelte b/front/src/Components/Chat/ChatMessageForm.svelte new file mode 100644 index 00000000..cd2ea66e --- /dev/null +++ b/front/src/Components/Chat/ChatMessageForm.svelte @@ -0,0 +1,56 @@ + + +
+ + +
+ + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatPlayerName.svelte b/front/src/Components/Chat/ChatPlayerName.svelte new file mode 100644 index 00000000..9b0630c0 --- /dev/null +++ b/front/src/Components/Chat/ChatPlayerName.svelte @@ -0,0 +1,51 @@ + + + + + {player.name} + + {#if isSubMenuOpen} + + {/if} + + + + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatSubMenu.svelte b/front/src/Components/Chat/ChatSubMenu.svelte new file mode 100644 index 00000000..6690699e --- /dev/null +++ b/front/src/Components/Chat/ChatSubMenu.svelte @@ -0,0 +1,33 @@ + + +
    +
  • +
  • +
+ + + \ No newline at end of file diff --git a/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte b/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte index 1807f15d..684975bb 100644 --- a/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte +++ b/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte @@ -1,11 +1,11 @@ + + +

Website opened by script.

+ + diff --git a/maps/tests/Metadata/cowebsiteAllowApi.js b/maps/tests/Metadata/cowebsiteAllowApi.js new file mode 100644 index 00000000..56c7b767 --- /dev/null +++ b/maps/tests/Metadata/cowebsiteAllowApi.js @@ -0,0 +1 @@ +WA.nav.openCoWebSite("cowebsiteAllowApi.html", true, ""); diff --git a/maps/tests/Metadata/cowebsiteAllowApi.json b/maps/tests/Metadata/cowebsiteAllowApi.json new file mode 100644 index 00000000..55ed615f --- /dev/null +++ b/maps/tests/Metadata/cowebsiteAllowApi.json @@ -0,0 +1,98 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":1, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":116.5, + "id":1, + "name":"", + "rotation":0, + "text": + { + "text":"Test : \nThe iframe is opened by script.\n\nResult : \nA message is send to the chat.", + "wrap":true + }, + "type":"", + "visible":true, + "width":295.875, + "x":11.8125, + "y":188.5 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 16, 16, 16, 0, 0, 16, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 16, 16, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 16, 16, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"mushroom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "nextlayerid":5, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"cowebsiteAllowApi.js" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"tileset_dungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/customMenu.html b/maps/tests/Metadata/customMenu.html index a80dca08..404673f3 100644 --- a/maps/tests/Metadata/customMenu.html +++ b/maps/tests/Metadata/customMenu.html @@ -1,13 +1,20 @@ - - - - + + +

Add a custom menu

\ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentRoom.html b/maps/tests/Metadata/getCurrentRoom.html index 7429b2a8..485f2ac8 100644 --- a/maps/tests/Metadata/getCurrentRoom.html +++ b/maps/tests/Metadata/getCurrentRoom.html @@ -1,16 +1,23 @@ - + - +

Log in the console the information of the current room

\ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentUser.html b/maps/tests/Metadata/getCurrentUser.html index 02be24f7..da0c3191 100644 --- a/maps/tests/Metadata/getCurrentUser.html +++ b/maps/tests/Metadata/getCurrentUser.html @@ -1,15 +1,22 @@ - + - +

Log in the console the information of the current player

\ No newline at end of file diff --git a/maps/tests/Metadata/map.json b/maps/tests/Metadata/map.json new file mode 100644 index 00000000..8967ed02 --- /dev/null +++ b/maps/tests/Metadata/map.json @@ -0,0 +1,230 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 33, 34, 34, 34, 34, 34, 34, 35, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 49, 50, 50, 50, 50, 50, 50, 51, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19], + "height":10, + "id":3, + "name":"wall", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/playerMove.html b/maps/tests/Metadata/playerMove.html index 339a3fd2..46a36845 100644 --- a/maps/tests/Metadata/playerMove.html +++ b/maps/tests/Metadata/playerMove.html @@ -1,12 +1,18 @@ - + -
- +

Log in the console the movement of the current player in the zone of the iframe

\ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js new file mode 100644 index 00000000..c857d783 --- /dev/null +++ b/maps/tests/Metadata/script.js @@ -0,0 +1,9 @@ + + +/*WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); +WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); +WA.getRoomId().then((roomId) => console.log('roomID : ',roomId));*/ + +//WA.onPlayerMove(console.log); +WA.setProperty('metadata', 'openWebsite', 'https://fr.wikipedia.org/'); +WA.getDataLayer().then((data) => {console.log('data 1 : ', data)}); \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.html b/maps/tests/Metadata/setProperty.html index 5259ec0a..c61aa5fa 100644 --- a/maps/tests/Metadata/setProperty.html +++ b/maps/tests/Metadata/setProperty.html @@ -1,12 +1,19 @@ - + - +

Change the url of this iframe and add the 'openWebsite' property to the red tile layer

\ No newline at end of file diff --git a/maps/tests/Metadata/setTiles.html b/maps/tests/Metadata/setTiles.html new file mode 100644 index 00000000..90b5a84d --- /dev/null +++ b/maps/tests/Metadata/setTiles.html @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/maps/tests/Metadata/setTiles.json b/maps/tests/Metadata/setTiles.json new file mode 100644 index 00000000..7eb9791a --- /dev/null +++ b/maps/tests/Metadata/setTiles.json @@ -0,0 +1,343 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"setTiles.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 65, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":8, + "name":"setTiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":191.866671635267, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass\n\nResult : \nThe Yellow Tile opens a Jitsi with Trigger.\n\nThe Red Tile opens cowebsite Wikipedia. The highest Red Tile is found by 'name' property index, the lowest by 'number' index.\n\nThe White Tiles are silent tiles. You cannot open a bubble in it. (Even if the other player didn't activate the script.)\n\nThe Pale Tile (Lowest) is an exitUrl tile to customMenu.json.\n\nThe Blue Tile are 'collides' tile. The two tiles in the center are 'number' index. The others are 'name' property index.\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":274.674838251912, + "x":32.5473600365393, + "y":128.305680721763 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tiles":[ + { + "id":9, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"customMenu.json" + }] + }, + { + "id":27, + "properties":[ + { + "name":"jitsiRoom", + "type":"string", + "value":"TEST" + }, + { + "name":"jitsiTrigger", + "type":"string", + "value":"onaction" + }] + }, + { + "id":34, + "properties":[ + { + "name":"name", + "type":"string", + "value":"Red" + }, + { + "name":"openWebsite", + "type":"string", + "value":"https:\/\/fr.wikipedia.org\/wiki\/Wikip%C3%A9dia:Accueil_principal" + }] + }, + { + "id":40, + "properties":[ + { + "name":"name", + "type":"string", + "value":"" + }] + }, + { + "id":44, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }, + { + "name":"name", + "type":"string", + "value":"blue" + }] + }, + { + "id":52, + "properties":[ + { + "name":"silent", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} diff --git a/maps/tests/Metadata/showHideLayer.html b/maps/tests/Metadata/showHideLayer.html index 4677f9e5..c6103722 100644 --- a/maps/tests/Metadata/showHideLayer.html +++ b/maps/tests/Metadata/showHideLayer.html @@ -1,21 +1,27 @@ - +
- \ No newline at end of file diff --git a/maps/tests/function_tiles.json b/maps/tests/function_tiles.json new file mode 100644 index 00000000..9bc374eb --- /dev/null +++ b/maps/tests/function_tiles.json @@ -0,0 +1,33 @@ +{ "columns":2, + "image":"function_tiles.png", + "imageheight":64, + "imagewidth":64, + "margin":0, + "name":"function_tiles", + "spacing":0, + "tilecount":4, + "tiledversion":"1.6.0", + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"start", + "type":"string", + "value":"S1" + }] + }, + { + "id":1, + "properties":[ + { + "name":"start", + "type":"string", + "value":"S2" + }] + }], + "tilewidth":32, + "type":"tileset", + "version":"1.6" +} \ No newline at end of file diff --git a/maps/tests/function_tiles.png b/maps/tests/function_tiles.png new file mode 100644 index 00000000..147eb619 Binary files /dev/null and b/maps/tests/function_tiles.png differ diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index aa8e55ec..038874b6 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -1,22 +1,30 @@ - +
diff --git a/maps/tests/index.html b/maps/tests/index.html index 0929ab83..df305cfa 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -4,7 +4,7 @@ - + @@ -106,6 +106,14 @@ Test the HelpCameraSettingScene + + + + - - - - + + + + + + + + + + + + + + + +
Result
+ Success Failure Pending + + Test a iframe opened by a script can use Iframe API +
Success Failure Pending @@ -114,14 +122,6 @@ Testing add a custom menu by scripting API
- Success Failure Pending - - Testing return current room attributes by Scripting API (Need to test from current user) -
Success Failure Pending @@ -130,6 +130,14 @@ Testing return current user attributes by Scripting API
+ Success Failure Pending + + Testing return current room attributes by Scripting API (Need to test from current user) +
Success Failure Pending @@ -162,10 +170,49 @@ Test animated tiles
+ Success Failure Pending + + Test start tile (S1) +
+ Success Failure Pending + + Test start tile (S2) +
+ Success Failure Pending + + Test set tiles +